XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;

namespace XFESystem.Browser.Tests;

public sealed class BrowserCspStylePolicyTests
{
    // The directive shape returned by Bing, with a fixed test nonce. This is
    // deliberately not an origin-specific exception in the browser.
    private const string ScriptOnlyPolicy = "script-src https: 'strict-dynamic' 'report-sample' 'wasm-unsafe-eval' 'nonce-gFxVK4F/aqAnFOE4KSUX6OzsblspGZEJdoCE8R5cPbY='; base-uri 'self';";
    private const string Body = """
        <link rel="stylesheet" href="/Theme.css">
        <style>#InlineSheet{color:#345678}</style>
        <p id="External">external text</p><p id="InlineSheet">inline sheet text</p>
        <p id="InlineAttribute" style="color:#56789a">inline attribute text</p>
        <p id="ScriptResult">safe original</p>
        <script nonce="gFxVK4F/aqAnFOE4KSUX6OzsblspGZEJdoCE8R5cPbY=">document.getElementById('ScriptResult').textContent='unsafe inline'</script>
        <script src="/Blocked.js"></script>
        """;

    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void ScriptOnlyPolicyAllowsStylesAndMatchingNonceButBlocksUnnoncedParserScript(bool meta)
    {
        var (browser, transport) = Open(meta ? null : ScriptOnlyPolicy, meta ? Meta(ScriptOnlyPolicy) : "");
        AssertStyled(browser, transport);
        Assert.DoesNotContain("safe original", browser.CurrentPage.Text, StringComparison.Ordinal);
        Assert.Contains("unsafe inline", browser.CurrentPage.Text, StringComparison.Ordinal);
        Assert.DoesNotContain("unsafe external", browser.CurrentPage.Text, StringComparison.Ordinal);
        Assert.DoesNotContain("Automatic scripts blocked", browser.CurrentPage.Text, StringComparison.Ordinal);
        Assert.DoesNotContain(transport.Requests, address => address.EndsWith("Blocked.js", StringComparison.Ordinal));
        Assert.False(browser.Capabilities.FullContentSecurityPolicy);
    }

    [Theory]
    [InlineData("default-src 'none'")]
    [InlineData("default-src https:")]
    [InlineData("style-src 'self'")]
    [InlineData("style-src-elem 'unsafe-inline'")]
    [InlineData("style-src-attr *")]
    [InlineData("STYLE-SRC 'none'")]
    public void AnyStyleOrDefaultDirectiveConservativelyBlocksExternalAndInlineStyles(string restriction)
    {
        var (browser, transport) = Open(ScriptOnlyPolicy + restriction);
        AssertBlocked(browser, transport);
    }

    [Theory]
    [InlineData("style-src 'none'", false)]
    [InlineData("default-src 'none'", true)]
    [InlineData("script-src 'none'; future-policy x", false)]
    public void EveryCommaSeparatedHeaderPolicyIntersectsEvenWhenFirstPolicyIsPermissive(string second, bool first)
    {
        string policies = first ? second + ", " + ScriptOnlyPolicy : ScriptOnlyPolicy + ", " + second;
        var (browser, transport) = Open(policies);
        AssertBlocked(browser, transport);
    }

    [Fact]
    public void HeaderAndAllMetaPoliciesIntersectInsteadOfOnlyReadingFirstMeta()
    {
        var (browser, transport) = Open(ScriptOnlyPolicy,
            Meta("script-src 'none'") + Meta("style-src 'none'") + Meta(ScriptOnlyPolicy));
        AssertBlocked(browser, transport);
        (browser, transport) = Open("style-src 'none'", Meta(ScriptOnlyPolicy));
        AssertBlocked(browser, transport);
    }

    [Fact]
    public void MultipleNonStyleHeaderAndMetaPoliciesStillAllowStyles()
    {
        var (browser, transport) = Open(ScriptOnlyPolicy + ", script-src 'none'; object-src 'none'",
            Meta("img-src https: data:; font-src 'self'") + Meta("connect-src https://api.example.test; frame-ancestors 'none'"));
        AssertStyled(browser, transport);
    }

    [Theory]
    [InlineData("")]
    [InlineData(" ")]
    [InlineData("script-src 'unterminated")]
    [InlineData("script-src 'nonce-'")]
    [InlineData("script-src 'nonce-a===b'")]
    [InlineData("script-src 'unrecognized-keyword'")]
    [InlineData("future-directive 'self'")]
    [InlineData("script-src https:; sandbox allow-scripts")]
    [InlineData("script-src https:\r\n style-src 'none'")]
    [InlineData("script-src https:, ")]
    [InlineData("script-src https:; report-to")]
    public void MalformedOrUnknownHeaderPolicyFailsClosed(string policy)
    {
        var (browser, transport) = Open(policy);
        AssertBlocked(browser, transport);
    }

    [Theory]
    [InlineData("<meta http-equiv='Content-Security-Policy'>")]
    [InlineData("<meta http-equiv='Content-Security-Policy' content=''>")]
    [InlineData("<meta http-equiv='Content-Security-Policy' content='unknown-directive x'>")]
    public void EmptyOrUnknownLaterMetaCannotRelaxHeader(string meta)
    {
        var (browser, transport) = Open(ScriptOnlyPolicy, meta);
        AssertBlocked(browser, transport);
    }

    [Theory]
    [InlineData("base-uri 'none'")]
    [InlineData("base-uri 'self'")]
    public void UnenforcedBaseUriWithDeclaredBaseCannotFetchStyles(string policy)
    {
        var (browser, transport) = Open("script-src 'none'; " + policy, "<base href='https://evil.example/'>");
        AssertBlocked(browser, transport);
        Assert.DoesNotContain(transport.Requests, address => address.Contains("evil.example", StringComparison.Ordinal));
    }

    [Fact]
    public void NavigationReplacesBothCspFlagsWithoutLeakingPreviousStylePermission()
    {
        var transport = new Fixture(ScriptOnlyPolicy, "");
        var browser = new BrowserService(transport);
        browser.Navigate("https://page.example/");
        AssertStyled(browser, transport);
        transport.Policy = "default-src 'none'";
        transport.Requests.Clear();
        browser.Navigate("https://page.example/");
        AssertBlocked(browser, transport);
        transport.Policy = null;
        transport.Requests.Clear();
        browser.Navigate("https://page.example/");
        AssertStyled(browser, transport);
        Assert.Contains("unsafe external", browser.CurrentPage.Text, StringComparison.Ordinal);
    }

    private static (BrowserService Browser, Fixture Transport) Open(string? header, string prefix = "")
    {
        var transport = new Fixture(header, prefix);
        var browser = new BrowserService(transport);
        browser.Navigate("https://page.example/");
        Assert.NotEqual("Navigation failed", browser.CurrentPage.Title);
        return (browser, transport);
    }

    private static void AssertStyled(BrowserService browser, Fixture transport)
    {
        var commands = browser.Layout(new(600, 400)).Commands;
        Assert.Contains(commands, command => command.Kind == BrowserDrawKind.Text && command.Text == "external" && command.Color == 0x123456);
        Assert.Contains(commands, command => command.Kind == BrowserDrawKind.Text && command.Text == "sheet" && command.Color == 0x345678);
        Assert.Contains(commands, command => command.Kind == BrowserDrawKind.Text && command.Text == "attribute" && command.Color == 0x56789A);
        Assert.Single(browser.LoadedResources, resource => resource.Kind == "style");
        Assert.Contains("https://page.example/Theme.css", transport.Requests);
    }

    private static void AssertBlocked(BrowserService browser, Fixture transport)
    {
        var list = browser.Layout(new(600, 400));
        Assert.Single(transport.Requests);
        Assert.Empty(browser.LoadedResources);
        Assert.DoesNotContain(list.Commands, command => command.Color is 0x123456 or 0x345678 or 0x56789A);
    }

    private static string Meta(string content) => "<meta http-equiv='Content-Security-Policy' content=\"" + WebUtility.HtmlEncode(content) + "\">";

    private sealed class Fixture(string? policy, string prefix) : IWebTransport
    {
        public string? Policy { get; set; } = policy;
        public List<string> Requests { get; } = [];
        public WebResponse Get(Uri address, BrowserLimits limits)
        {
            Requests.Add(address.AbsoluteUri);
            return address.AbsolutePath switch
            {
                "/Theme.css" => new(address, 200, "text/css", "#External{color:#123456}", true),
                "/Blocked.js" => new(address, 200, "text/javascript", "document.getElementById('ScriptResult').textContent='unsafe external'", true),
                _ => new(address, 200, "text/html", prefix + Body, true, Policy)
            };
        }
    }
}