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 builtin = @import("builtin");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const xdg = @import("../../os/main.zig").xdg; const internal_os = @import("../../os/main.zig");
const TempDir = @import("../../os/main.zig").TempDir; const xdg = internal_os.xdg;
const TempDir = internal_os.TempDir;
const Entry = @import("Entry.zig"); const Entry = @import("Entry.zig");
// 512KB - sufficient for approximately 10k entries // 512KB - sufficient for approximately 10k entries
@ -332,48 +333,28 @@ fn isValidCacheKey(key: []const u8) bool {
if (std.mem.indexOf(u8, key, "@")) |at_pos| { if (std.mem.indexOf(u8, key, "@")) |at_pos| {
const user = key[0..at_pos]; const user = key[0..at_pos];
const hostname = key[at_pos + 1 ..]; 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 // Checks if a host is a valid hostname or IP address
// (including IPv6 in brackets) fn isValidHost(host: []const u8) bool {
fn isValidHostname(host: []const u8) bool { // First check for valid hostnames because this is assumed to be the more
if (host.len == 0 or host.len > 253) return false; // likely ssh host format.
if (internal_os.hostname.isValid(host)) {
// Handle IPv6 addresses in brackets return true;
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;
} }
// Standard hostname/domain validation // We also accept valid IP addresses. In practice, IPv4 addresses are also
for (host) |c| { // considered valid hostnames due to their overlapping syntax, so we can
switch (c) { // simplify this check to be IPv6-specific.
'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {}, if (std.net.Address.parseIp6(host, 0)) |_| {
else => return false, return true;
} } else |_| {
}
// 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] == '-')
{
return false; return false;
} }
return std.mem.indexOf(u8, host, "..") == null;
} }
fn isValidUser(user: []const u8) bool { fn isValidUser(user: []const u8) bool {
@ -467,42 +448,36 @@ test "disk cache operations" {
} }
// Tests // 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; 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" { // Valid hostnames
const testing = std.testing; try testing.expect(isValidHost("localhost"));
try testing.expect(!isValidHostname("")); try testing.expect(isValidHost("example.com"));
try testing.expect(!isValidHostname("host\nname")); try testing.expect(isValidHost("sub.example.com"));
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"));
// Too long // IPv4 addresses
const long_host = "a" ** 254; try testing.expect(isValidHost("127.0.0.1"));
try testing.expect(!isValidHostname(long_host)); 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" { 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("example.com"));
try testing.expect(isValidCacheKey("sub.example.com")); try testing.expect(isValidCacheKey("sub.example.com"));
try testing.expect(isValidCacheKey("192.168.1.1")); 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(""));
try testing.expect(!isValidCacheKey(".invalid.com")); 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("test-user@192.168.1.1"));
try testing.expect(isValidCacheKey("user_name@host.domain.org")); try testing.expect(isValidCacheKey("user_name@host.domain.org"));
try testing.expect(isValidCacheKey("git@github.com")); 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("@example.com"));
try testing.expect(!isValidCacheKey("user@")); try testing.expect(!isValidCacheKey("user@"));
try testing.expect(!isValidCacheKey("user@@host")); try testing.expect(!isValidCacheKey("user@@host"));