Expand URL risk and browser proxy tests

Add coverage for HttpUrlRiskEvaluator boundary cases and BrowserProxy capability path/port/query behavior.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
github-actions[bot] 2026-05-05 10:26:43 -07:00 committed by GitHub
parent 0b66d5a10c
commit 324669d8e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 194 additions and 0 deletions

View File

@ -889,6 +889,111 @@ public class BrowserProxyCapabilityTests
Assert.Contains("ssh -N -L 28791:127.0.0.1:18791", res.Error);
}
[Fact]
public async Task BrowserProxy_EmptyPath_ReturnsError()
{
var cap = new BrowserProxyCapability(
NullLogger.Instance,
"ws://127.0.0.1:18789",
"token",
new CapturingHandler("""{"ok":true}"""));
var res = await cap.ExecuteAsync(new NodeInvokeRequest
{
Id = "bp-empty-path",
Command = "browser.proxy",
Args = Parse("""{"path":""}""")
});
Assert.False(res.Ok);
Assert.Contains("path required", res.Error);
}
[Fact]
public async Task BrowserProxy_SlashlessPrependedPath_NormalizesWithLeadingSlash()
{
var handler = new CapturingHandler("""{"ok":true}""");
var cap = new BrowserProxyCapability(
NullLogger.Instance,
"ws://127.0.0.1:18789",
"token",
handler);
var res = await cap.ExecuteAsync(new NodeInvokeRequest
{
Id = "bp-no-leading-slash",
Command = "browser.proxy",
Args = Parse("""{"path":"snapshot"}""")
});
Assert.True(res.Ok);
Assert.NotNull(handler.LastRequest);
Assert.StartsWith("/snapshot", handler.LastRequest!.RequestUri!.AbsolutePath);
}
[Fact]
public async Task BrowserProxy_DoubleSlashPath_ReturnsError()
{
var cap = new BrowserProxyCapability(
NullLogger.Instance,
"ws://127.0.0.1:18789",
"token",
new CapturingHandler("""{"ok":true}"""));
var res = await cap.ExecuteAsync(new NodeInvokeRequest
{
Id = "bp-double-slash",
Command = "browser.proxy",
Args = Parse("""{"path":"//evil.com/inject"}""")
});
Assert.False(res.Ok);
Assert.Contains("local control path", res.Error);
}
[Fact]
public async Task BrowserProxy_GatewayPortAbove65533_ReturnsError()
{
var cap = new BrowserProxyCapability(
NullLogger.Instance,
"ws://127.0.0.1:65534", // control port would be 65536 — out of range
"token",
new CapturingHandler("""{"ok":true}"""));
var res = await cap.ExecuteAsync(new NodeInvokeRequest
{
Id = "bp-port-overflow",
Command = "browser.proxy",
Args = Parse("""{"path":"/"}""")
});
Assert.False(res.Ok);
Assert.Contains("port", res.Error);
}
[Fact]
public async Task BrowserProxy_QueryAndProfileAppendedToUri()
{
var handler = new CapturingHandler("""{"ok":true}""");
var cap = new BrowserProxyCapability(
NullLogger.Instance,
"ws://127.0.0.1:18789",
"token",
handler);
var res = await cap.ExecuteAsync(new NodeInvokeRequest
{
Id = "bp-query-profile",
Command = "browser.proxy",
Args = Parse("""{"path":"/tabs","query":{"active":"true"},"profile":"work"}""")
});
Assert.True(res.Ok);
var requestUri = handler.LastRequest!.RequestUri!.ToString();
Assert.Contains("active=true", requestUri);
Assert.Contains("profile=work", requestUri);
}
private sealed class CapturingHandler : HttpMessageHandler
{
private readonly string _response;

View File

@ -66,4 +66,93 @@ public class HttpUrlRiskEvaluatorTests
Assert.True(HttpUrlRiskEvaluator.IsPublicAddress(ip),
$"Expected {ipString} to be classified as public");
}
// ── IsPublicAddress: IPv4 ────────────────────────────────────────────────
[Theory]
[InlineData("0.1.2.3")] // 0.0.0.0/8 — "this" network
[InlineData("10.0.0.1")] // RFC 1918 class A
[InlineData("10.255.255.255")] // RFC 1918 class A — boundary
[InlineData("100.64.0.1")] // CGNAT 100.64.0.0/10
[InlineData("100.127.255.255")] // CGNAT upper boundary
[InlineData("127.0.0.1")] // loopback
[InlineData("127.255.255.255")] // loopback range boundary
[InlineData("169.254.0.1")] // link-local
[InlineData("169.254.255.255")] // link-local boundary
[InlineData("172.16.0.1")] // RFC 1918 class B lower
[InlineData("172.31.255.255")] // RFC 1918 class B upper boundary
[InlineData("192.168.0.1")] // RFC 1918 class C
[InlineData("192.168.255.255")] // RFC 1918 class C boundary
[InlineData("224.0.0.1")] // multicast lower
[InlineData("239.255.255.255")] // multicast upper boundary
[InlineData("255.255.255.255")] // limited broadcast (>= 224)
public void IsPublicAddress_NonPublicIPv4_ReturnsFalse(string ipString)
{
var ip = IPAddress.Parse(ipString);
Assert.False(HttpUrlRiskEvaluator.IsPublicAddress(ip),
$"Expected {ipString} to be classified as non-public");
}
[Theory]
[InlineData("8.8.8.8")] // Google DNS
[InlineData("1.1.1.1")] // Cloudflare DNS
[InlineData("203.0.113.1")] // TEST-NET-3 — still public by the classifier's rules
[InlineData("172.32.0.1")] // just outside RFC 1918 class B range
[InlineData("172.15.255.255")] // just below RFC 1918 class B range
[InlineData("100.63.255.255")] // just below CGNAT range
[InlineData("100.128.0.1")] // just above CGNAT range
public void IsPublicAddress_PublicIPv4_ReturnsTrue(string ipString)
{
var ip = IPAddress.Parse(ipString);
Assert.True(HttpUrlRiskEvaluator.IsPublicAddress(ip),
$"Expected {ipString} to be classified as public");
}
[Theory]
[InlineData("::ffff:8.8.8.8")] // IPv4-mapped public
[InlineData("::ffff:1.1.1.1")] // IPv4-mapped public (Cloudflare)
public void IsPublicAddress_IPv4MappedPublic_ReturnsTrue(string ipString)
{
var ip = IPAddress.Parse(ipString);
Assert.True(HttpUrlRiskEvaluator.IsPublicAddress(ip),
$"Expected IPv4-mapped public address {ipString} to be classified as public");
}
// ── Evaluate: additional hostname / address forms ─────────────────────────
[Fact]
public void Evaluate_Localhost_RequiresConfirmationWithLocalhostReason()
{
var risk = HttpUrlRiskEvaluator.Evaluate("https://localhost:3000/");
Assert.True(risk.RequiresConfirmation);
Assert.Contains(risk.Reasons, r => r.Contains("localhost", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Evaluate_PublicIPv6Literal_RequiresConfirmationWithIpLiteralReason()
{
var risk = HttpUrlRiskEvaluator.Evaluate("https://[2606:4700:4700::1111]/");
Assert.True(risk.RequiresConfirmation);
Assert.Contains(risk.Reasons, r => r.Contains("IP literal", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Evaluate_HostKeyIsLowercased()
{
// Port 443 is the default for HTTPS, so uri.Authority omits it.
// Use a non-default port to verify the port is included in HostKey.
var risk = HttpUrlRiskEvaluator.Evaluate("https://Example.COM:8443/");
Assert.Equal("example.com:8443", risk.HostKey);
}
[Fact]
public void Evaluate_CanonicalOriginIncludesNonStandardPort()
{
var risk = HttpUrlRiskEvaluator.Evaluate("https://example.com:8443/path");
Assert.Equal("https://example.com:8443/", risk.CanonicalOrigin);
}
}