From b1a696c62d2fd74c84b8d7e5e97e2b153722338c Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 5 Nov 2025 08:17:27 -0500 Subject: [PATCH] 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). --- lib/std/Io/net/HostName.zig | 79 ++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 35aeff6942fe..f84c70e723a6 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -24,15 +24,82 @@ pub const ValidateError = error{ InvalidHostName, }; +/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) pub fn validate(bytes: []const u8) ValidateError!void { - if (bytes.len > max_len) return error.NameTooLong; - if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName; - for (bytes) |byte| { - if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { - continue; + if (bytes.len == 0) return error.InvalidHostName; + if (bytes[0] == '.') return error.InvalidHostName; + + // Ignore trailing dot (FQDN). It doesn't count toward our length. + const end = if (bytes[bytes.len - 1] == '.') end: { + if (bytes.len == 1) return error.InvalidHostName; + break :end bytes.len - 1; + } else bytes.len; + + // The accepted maximum length of a hostname, including labels and dots. + if (end > max_len) return error.NameTooLong; + + // Hostnames are divided into dot-separated "labels", which: + // + // - Start with a letter or digit + // - Can contain letters, digits, or hyphens + // - Must end with a letter or digit + // - Have a minimum of 1 character and a maximum of 63 + var label_start: usize = 0; + var label_len: usize = 0; + for (bytes[0..end], 0..) |c, i| { + switch (c) { + '.' => { + if (label_len == 0 or label_len > 63) return error.InvalidHostName; + if (!std.ascii.isAlphanumeric(bytes[label_start])) return error.InvalidHostName; + if (!std.ascii.isAlphanumeric(bytes[i - 1])) return error.InvalidHostName; + + label_start = i + 1; + label_len = 0; + }, + '-' => { + label_len += 1; + }, + else => { + if (!std.ascii.isAlphanumeric(c)) return error.InvalidHostName; + label_len += 1; + }, } - return error.InvalidHostName; } + + // Validate the final label + if (label_len == 0 or label_len > 63) return error.InvalidHostName; + if (!std.ascii.isAlphanumeric(bytes[label_start])) return error.InvalidHostName; + if (!std.ascii.isAlphanumeric(bytes[end - 1])) return error.InvalidHostName; +} + +test validate { + // Valid hostnames + try validate("example"); + try validate("example.com"); + try validate("www.example.com"); + try validate("sub.domain.example.com"); + try validate("example.com."); + try validate("host-name.example.com."); + try validate("123.example.com."); + try validate("a-b.com"); + try validate("a.b.c.d.e.f.g"); + try validate("127.0.0.1"); // Also a valid hostname + try validate("a" ** 63 ++ ".com"); // Label exactly 63 chars (valid) + try validate("a." ** 127 ++ "a"); // Total length 255 (valid) + + // Invalid hostnames + try std.testing.expectError(error.InvalidHostName, validate("")); + try std.testing.expectError(error.InvalidHostName, validate(".example.com")); + try std.testing.expectError(error.InvalidHostName, validate("example.com..")); + try std.testing.expectError(error.InvalidHostName, validate("host..domain")); + try std.testing.expectError(error.InvalidHostName, validate("-hostname")); + try std.testing.expectError(error.InvalidHostName, validate("hostname-")); + try std.testing.expectError(error.InvalidHostName, validate("a.-.b")); + try std.testing.expectError(error.InvalidHostName, validate("host_name.com")); + try std.testing.expectError(error.InvalidHostName, validate(".")); + try std.testing.expectError(error.InvalidHostName, validate("..")); + try std.testing.expectError(error.InvalidHostName, validate("a" ** 64 ++ ".com")); // Label length 64 (too long) + try std.testing.expectError(error.NameTooLong, validate("a." ** 127 ++ "ab")); // Total length 256 (too long) } pub fn init(bytes: []const u8) ValidateError!HostName {