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:
parent
0b66d5a10c
commit
324669d8e4
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user