From 53635c7fdb63d74dda6cebd994fe3f3b012a00f8 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 10 Aug 2026 15:35:06 -0500 Subject: [PATCH] reject special-purpose identity destinations --- src/internal/identity/network_safety.zig | 93 +++++++++++++++++++----- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/src/internal/identity/network_safety.zig b/src/internal/identity/network_safety.zig index efe916e..eab3c77 100644 --- a/src/internal/identity/network_safety.zig +++ b/src/internal/identity/network_safety.zig @@ -183,30 +183,61 @@ fn stripTrailingDot(host: []const u8) []const u8 { } fn isNonRoutableIp4(ip: [4]u8) bool { - return ip[0] == 0 or - ip[0] == 10 or - ip[0] == 127 or - (ip[0] == 169 and ip[1] == 254) or - (ip[0] == 172 and ip[1] >= 16 and ip[1] <= 31) or - (ip[0] == 192 and ip[1] == 168); + const blocked = [_]Ip4Range{ + .{ .network = .{ 0, 0, 0, 0 }, .prefix = 8 }, + .{ .network = .{ 10, 0, 0, 0 }, .prefix = 8 }, + .{ .network = .{ 100, 64, 0, 0 }, .prefix = 10 }, + .{ .network = .{ 127, 0, 0, 0 }, .prefix = 8 }, + .{ .network = .{ 169, 254, 0, 0 }, .prefix = 16 }, + .{ .network = .{ 172, 16, 0, 0 }, .prefix = 12 }, + .{ .network = .{ 192, 0, 0, 0 }, .prefix = 24 }, + .{ .network = .{ 192, 0, 2, 0 }, .prefix = 24 }, + .{ .network = .{ 192, 88, 99, 0 }, .prefix = 24 }, + .{ .network = .{ 192, 168, 0, 0 }, .prefix = 16 }, + .{ .network = .{ 198, 18, 0, 0 }, .prefix = 15 }, + .{ .network = .{ 198, 51, 100, 0 }, .prefix = 24 }, + .{ .network = .{ 203, 0, 113, 0 }, .prefix = 24 }, + .{ .network = .{ 224, 0, 0, 0 }, .prefix = 4 }, + .{ .network = .{ 240, 0, 0, 0 }, .prefix = 4 }, + }; + for (blocked) |range| if (range.contains(ip)) return true; + return false; } +const Ip4Range = struct { + network: [4]u8, + prefix: u5, + + fn contains(self: Ip4Range, address: [4]u8) bool { + const network = std.mem.readInt(u32, &self.network, .big); + const candidate = std.mem.readInt(u32, &address, .big); + const shift: u5 = @intCast(32 - @as(u6, self.prefix)); + const mask = if (self.prefix == 0) @as(u32, 0) else ~@as(u32, 0) << shift; + return network & mask == candidate & mask; + } +}; + fn isNonRoutableIp6(ip: [16]u8) bool { - const all_zero = for (ip) |b| { - if (b != 0) break false; - } else true; - - return all_zero or - isIp6Loopback(ip) or - (ip[0] == 0xfe and (ip[1] & 0xc0) == 0x80) or // fe80::/10 link-local - (ip[0] & 0xfe) == 0xfc; // fc00::/7 unique local + // IPv4-mapped values inherit the complete IPv4 policy. + if (ip4FromIp6Mapped(ip)) |ip4| return isNonRoutableIp4(ip4); + + // Identity fetches need globally routed unicast. Exclude the IETF + // special-purpose block, documentation space, and 6to4 even though they + // sit inside the broad 2000::/3 global-unicast allocation. + if (ip[0] & 0xe0 != 0x20) return true; + if (matchesIp6(ip, .{ 0x20, 0x01 } ++ .{0} ** 14, 23)) return true; + if (matchesIp6(ip, .{ 0x20, 0x01, 0x0d, 0xb8 } ++ .{0} ** 12, 32)) return true; + if (matchesIp6(ip, .{ 0x20, 0x02 } ++ .{0} ** 14, 16)) return true; + return false; } -fn isIp6Loopback(ip: [16]u8) bool { - for (ip[0..15]) |b| { - if (b != 0) return false; - } - return ip[15] == 1; +fn matchesIp6(address: [16]u8, network: [16]u8, prefix: u8) bool { + const whole_bytes = prefix / 8; + const remaining = prefix % 8; + if (!std.mem.eql(u8, address[0..whole_bytes], network[0..whole_bytes])) return false; + if (remaining == 0) return true; + const mask: u8 = @as(u8, 0xff) << @intCast(8 - remaining); + return address[whole_bytes] & mask == network[whole_bytes] & mask; } fn ip4FromIp6Mapped(ip: [16]u8) ?[4]u8 { @@ -269,6 +300,30 @@ test "identity host rejects obvious non-routable hosts" { try checkIdentityHost("8.8.8.8"); } +test "identity host rejects every special-purpose destination class" { + for ([_][]const u8{ + "0.0.0.1", + "100.100.100.200", + "192.0.0.1", + "192.0.2.1", + "192.88.99.1", + "198.18.0.1", + "198.51.100.1", + "203.0.113.1", + "224.0.0.1", + "255.255.255.255", + "2001::1", + "2001:db8::1", + "2002:7f00:1::", + "::ffff:100.100.100.200", + }) |host| try std.testing.expectError( + error.UnsafeIdentityHost, + checkIdentityHost(host), + ); + try checkIdentityHost("1.1.1.1"); + try checkIdentityHost("2606:4700:4700::1111"); +} + test "identity url check rejects literal localhost" { try std.testing.expectError(error.UnsafeIdentityHost, checkIdentityUrl("https://127.0.0.1/.well-known/did.json")); } -- 2.51.2