cli: fix +ssh-cache IPv6 address validation (#9281)

The host validation code previously expected IPv6 addresses to be
enclosed in [brackets], but that's not how ssh(1) expects them.

This change removes that requirement and reimplements the host
validation routine to check for valid hostnames and IP addresses (IPv4
and IPv6) using standard routines rather than custom logic.

Fixes #9251
1.2.x
Mitchell Hashimoto 2025-10-19 19:47:41 -07:00
parent 508cc8300a
commit ed91bdadd6
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 45 additions and 70 deletions

View File

@ -7,8 +7,9 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const xdg = @import("../../os/main.zig").xdg;
const TempDir = @import("../../os/main.zig").TempDir;
const internal_os = @import("../../os/main.zig");
const xdg = internal_os.xdg;
const TempDir = internal_os.TempDir;
const Entry = @import("Entry.zig");
// 512KB - sufficient for approximately 10k entries
@ -332,48 +333,28 @@ fn isValidCacheKey(key: []const u8) bool {
if (std.mem.indexOf(u8, key, "@")) |at_pos| {
const user = key[0..at_pos];
const hostname = key[at_pos + 1 ..];
return isValidUser(user) and isValidHostname(hostname);
return isValidUser(user) and isValidHost(hostname);
}
return isValidHostname(key);
return isValidHost(key);
}
// Basic hostname validation - accepts domains and IPs
// (including IPv6 in brackets)
fn isValidHostname(host: []const u8) bool {
if (host.len == 0 or host.len > 253) return false;
// Handle IPv6 addresses in brackets
if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') {
const ipv6_part = host[1 .. host.len - 1];
if (ipv6_part.len == 0) return false;
var has_colon = false;
for (ipv6_part) |c| {
switch (c) {
'a'...'f', 'A'...'F', '0'...'9' => {},
':' => has_colon = true,
else => return false,
}
}
return has_colon;
// Checks if a host is a valid hostname or IP address
fn isValidHost(host: []const u8) bool {
// First check for valid hostnames because this is assumed to be the more
// likely ssh host format.
if (internal_os.hostname.isValid(host)) {
return true;
}
// Standard hostname/domain validation
for (host) |c| {
switch (c) {
'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {},
else => return false,
}
}
// No leading/trailing dots or hyphens, no consecutive dots
if (host[0] == '.' or host[0] == '-' or
host[host.len - 1] == '.' or host[host.len - 1] == '-')
{
// We also accept valid IP addresses. In practice, IPv4 addresses are also
// considered valid hostnames due to their overlapping syntax, so we can
// simplify this check to be IPv6-specific.
if (std.net.Address.parseIp6(host, 0)) |_| {
return true;
} else |_| {
return false;
}
return std.mem.indexOf(u8, host, "..") == null;
}
fn isValidUser(user: []const u8) bool {
@ -467,42 +448,36 @@ test "disk cache operations" {
}
// Tests
test "hostname validation - valid cases" {
const testing = std.testing;
try testing.expect(isValidHostname("example.com"));
try testing.expect(isValidHostname("sub.example.com"));
try testing.expect(isValidHostname("host-name.domain.org"));
try testing.expect(isValidHostname("192.168.1.1"));
try testing.expect(isValidHostname("a"));
try testing.expect(isValidHostname("1"));
}
test "hostname validation - IPv6 addresses" {
test isValidHost {
const testing = std.testing;
try testing.expect(isValidHostname("[::1]"));
try testing.expect(isValidHostname("[2001:db8::1]"));
try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported
try testing.expect(!isValidHostname("[]")); // Empty IPv6
try testing.expect(!isValidHostname("[invalid]")); // No colons
}
test "hostname validation - invalid cases" {
const testing = std.testing;
try testing.expect(!isValidHostname(""));
try testing.expect(!isValidHostname("host\nname"));
try testing.expect(!isValidHostname(".example.com"));
try testing.expect(!isValidHostname("example.com."));
try testing.expect(!isValidHostname("host..domain"));
try testing.expect(!isValidHostname("-hostname"));
try testing.expect(!isValidHostname("hostname-"));
try testing.expect(!isValidHostname("host name"));
try testing.expect(!isValidHostname("host_name"));
try testing.expect(!isValidHostname("host@domain"));
try testing.expect(!isValidHostname("host:port"));
// Valid hostnames
try testing.expect(isValidHost("localhost"));
try testing.expect(isValidHost("example.com"));
try testing.expect(isValidHost("sub.example.com"));
// Too long
const long_host = "a" ** 254;
try testing.expect(!isValidHostname(long_host));
// IPv4 addresses
try testing.expect(isValidHost("127.0.0.1"));
try testing.expect(isValidHost("192.168.1.1"));
// IPv6 addresses
try testing.expect(isValidHost("::1"));
try testing.expect(isValidHost("2001:db8::1"));
try testing.expect(isValidHost("2001:db8:0:1:1:1:1:1"));
try testing.expect(!isValidHost("fe80::1%eth0")); // scopes not supported
// Invalid hosts
try testing.expect(!isValidHost(""));
try testing.expect(!isValidHost("host\nname"));
try testing.expect(!isValidHost(".example.com"));
try testing.expect(!isValidHost("host..domain"));
try testing.expect(!isValidHost("-hostname"));
try testing.expect(!isValidHost("hostname-"));
try testing.expect(!isValidHost("host name"));
try testing.expect(!isValidHost("host_name"));
try testing.expect(!isValidHost("host@domain"));
try testing.expect(!isValidHost("host:port"));
}
test "user validation - valid cases" {
@ -543,7 +518,7 @@ test "cache key validation - hostname format" {
try testing.expect(isValidCacheKey("example.com"));
try testing.expect(isValidCacheKey("sub.example.com"));
try testing.expect(isValidCacheKey("192.168.1.1"));
try testing.expect(isValidCacheKey("[::1]"));
try testing.expect(isValidCacheKey("::1"));
try testing.expect(!isValidCacheKey(""));
try testing.expect(!isValidCacheKey(".invalid.com"));
}
@ -555,7 +530,7 @@ test "cache key validation - user@hostname format" {
try testing.expect(isValidCacheKey("test-user@192.168.1.1"));
try testing.expect(isValidCacheKey("user_name@host.domain.org"));
try testing.expect(isValidCacheKey("git@github.com"));
try testing.expect(isValidCacheKey("ubuntu@[::1]"));
try testing.expect(isValidCacheKey("ubuntu@::1"));
try testing.expect(!isValidCacheKey("@example.com"));
try testing.expect(!isValidCacheKey("user@"));
try testing.expect(!isValidCacheKey("user@@host"));