authorgravatar for jon@indelible.orgJon Parise <jon@indelible.org> 2025-11-05 08:17:27-05:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-08 20:40:32-08:00
logefe649b13e582be855376944bac1346426e238d6
tree21b5fbee7d58880fa30d0be03d9b26482b7a7787
parent6069161a51c4672b37c87d8cda06999baf5fef2c

std.Io.net: make HostName.validate RFC 1123-compliant

The implementation of HostName.validate was too generous. It considered strings like ".example.com", "exa..mple.com", and "-example.com" to be valid hostnames, which is incorrect according to RFC 1123 (the currently accepted standard). Reviewed-on: https://github.com/ziglang/zig/pull/25710

1 files changed, 73 insertions(+), 6 deletions(-)

lib/std/Io/net/HostName.zig+73-6
......@@ -24,15 +24,82 @@ pub const ValidateError = error{
2424 InvalidHostName,
2525};
2626
27/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123)
2728pub fn validate(bytes: []const u8) ValidateError!void {
28 if (bytes.len > max_len) return error.NameTooLong;
29 if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName;
30 for (bytes) |byte| {
31 if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) {
32 continue;
29 if (bytes.len == 0) return error.InvalidHostName;
30 if (bytes[0] == '.') return error.InvalidHostName;
31
32 // Ignore trailing dot (FQDN). It doesn't count toward our length.
33 const end = if (bytes[bytes.len - 1] == '.') end: {
34 if (bytes.len == 1) return error.InvalidHostName;
35 break :end bytes.len - 1;
36 } else bytes.len;
37
38 // The accepted maximum length of a hostname, including labels and dots.
39 if (end > max_len) return error.NameTooLong;
40
41 // Hostnames are divided into dot-separated "labels", which:
42 //
43 // - Start with a letter or digit
44 // - Can contain letters, digits, or hyphens
45 // - Must end with a letter or digit
46 // - Have a minimum of 1 character and a maximum of 63
47 var label_start: usize = 0;
48 var label_len: usize = 0;
49 for (bytes[0..end], 0..) |c, i| {
50 switch (c) {
51 '.' => {
52 if (label_len == 0 or label_len > 63) return error.InvalidHostName;
53 if (!std.ascii.isAlphanumeric(bytes[label_start])) return error.InvalidHostName;
54 if (!std.ascii.isAlphanumeric(bytes[i - 1])) return error.InvalidHostName;
55
56 label_start = i + 1;
57 label_len = 0;
58 },
59 '-' => {
60 label_len += 1;
61 },
62 else => {
63 if (!std.ascii.isAlphanumeric(c)) return error.InvalidHostName;
64 label_len += 1;
65 },
3366 }
34 return error.InvalidHostName;
3567 }
68
69 // Validate the final label
70 if (label_len == 0 or label_len > 63) return error.InvalidHostName;
71 if (!std.ascii.isAlphanumeric(bytes[label_start])) return error.InvalidHostName;
72 if (!std.ascii.isAlphanumeric(bytes[end - 1])) return error.InvalidHostName;
73}
74
75test validate {
76 // Valid hostnames
77 try validate("example");
78 try validate("example.com");
79 try validate("www.example.com");
80 try validate("sub.domain.example.com");
81 try validate("example.com.");
82 try validate("host-name.example.com.");
83 try validate("123.example.com.");
84 try validate("a-b.com");
85 try validate("a.b.c.d.e.f.g");
86 try validate("127.0.0.1"); // Also a valid hostname
87 try validate("a" ** 63 ++ ".com"); // Label exactly 63 chars (valid)
88 try validate("a." ** 127 ++ "a"); // Total length 255 (valid)
89
90 // Invalid hostnames
91 try std.testing.expectError(error.InvalidHostName, validate(""));
92 try std.testing.expectError(error.InvalidHostName, validate(".example.com"));
93 try std.testing.expectError(error.InvalidHostName, validate("example.com.."));
94 try std.testing.expectError(error.InvalidHostName, validate("host..domain"));
95 try std.testing.expectError(error.InvalidHostName, validate("-hostname"));
96 try std.testing.expectError(error.InvalidHostName, validate("hostname-"));
97 try std.testing.expectError(error.InvalidHostName, validate("a.-.b"));
98 try std.testing.expectError(error.InvalidHostName, validate("host_name.com"));
99 try std.testing.expectError(error.InvalidHostName, validate("."));
100 try std.testing.expectError(error.InvalidHostName, validate(".."));
101 try std.testing.expectError(error.InvalidHostName, validate("a" ** 64 ++ ".com")); // Label length 64 (too long)
102 try std.testing.expectError(error.NameTooLong, validate("a." ** 127 ++ "ab")); // Total length 256 (too long)
36103}
37104
38105pub fn init(bytes: []const u8) ValidateError!HostName {