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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;

namespace XFESystem.Browser;

/// <summary>
/// This is not a general CSP implementation. Author styles are allowed only
/// when every understood policy leaves them unrestricted; automatic scripts
/// remain disabled whenever any CSP is declared.
/// </summary>
internal static class BrowserCspStylePolicy
{
    internal readonly record struct Decision(bool Declared, bool BlockAuthorStyles);

    public static Decision Evaluate(string? header, IDocument document)
    {
        bool declared = header is not null, block = false;
        bool hasBase = document.QuerySelector("base[href]") is not null;
        if (header is not null) block = !LeavesStylesUnrestricted(header, hasBase);
        foreach (var meta in document.QuerySelectorAll("meta"))
        {
            if (!string.Equals(meta.GetAttribute("http-equiv"), "Content-Security-Policy", StringComparison.OrdinalIgnoreCase)) continue;
            declared = true;
            // All policies apply. A later permissive meta cannot undo a header
            // or an earlier meta, and a missing content value is not trusted.
            if (!LeavesStylesUnrestricted(meta.GetAttribute("content") ?? "", hasBase)) block = true;
        }
        return new(declared, block);
    }

    private static bool LeavesStylesUnrestricted(string policies, bool hasBase)
    {
        if (policies.Length is 0 or > 65_536) return false;
        foreach (char c in policies)
            if (c != '\t' && (c < ' ' || c > '~')) return false;
        // A comma separates independently enforced header policies. Rejecting
        // ambiguous commas in source values is intentional in this profile.
        var policyList = policies.Split(',');
        if (policyList.Length > 64) return false;
        foreach (string policy in policyList)
        {
            bool hasDirective = false;
            var directives = policy.Split(';');
            if (directives.Length > 128) return false;
            foreach (string directive in directives)
            {
                var tokens = directive.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
                if (tokens.Length == 0) continue;
                hasDirective = true;
                string name = tokens[0].ToLowerInvariant();
                if (name is "default-src" or "style-src" or "style-src-elem" or "style-src-attr") return false;
                bool script = name is "script-src" or "script-src-elem" or "script-src-attr";
                bool sourceList = script || name is "base-uri" or "child-src" or "connect-src" or "font-src" or
                    "frame-src" or "img-src" or "manifest-src" or "media-src" or "object-src" or "worker-src" or
                    "frame-ancestors" or "form-action";
                if (sourceList)
                {
                    // ResourceAddress does not yet enforce base-uri. Do not
                    // resolve a stylesheet against an unverified <base>.
                    if (name == "base-uri" && hasBase) return false;
                    for (int i = 1; i < tokens.Length; i++)
                        if (!ValidSource(tokens[i], script)) return false;
                }
                else if (name == "report-uri")
                {
                    if (tokens.Length < 2) return false;
                    for (int i = 1; i < tokens.Length; i++)
                        if (!Uri.TryCreate(tokens[i], UriKind.RelativeOrAbsolute, out _) || tokens[i].Contains('\'')) return false;
                }
                else if (name == "report-to")
                {
                    if (tokens.Length != 2 || !tokens[1].All(c => char.IsAsciiLetterOrDigit(c) || c is '-' or '_' or '.')) return false;
                }
                else return false; // Unknown semantics cannot relax the profile.
            }
            if (!hasDirective) return false;
        }
        return true;
    }

    private static bool ValidSource(string token, bool script)
    {
        if (token.Length is 0 or > 2_048) return false;
        if (token[0] == '\'')
        {
            if (token.Length < 3 || token[^1] != '\'') return false;
            string value = token[1..^1];
            if (value is "self" or "none") return true;
            if (!script) return false;
            if (value is "strict-dynamic" or "report-sample" or "unsafe-inline" or "unsafe-eval" or
                "wasm-unsafe-eval" or "unsafe-hashes" or "inline-speculation-rules") return true;
            int prefix = value.StartsWith("nonce-", StringComparison.Ordinal) ? 6 :
                value.StartsWith("sha256-", StringComparison.Ordinal) || value.StartsWith("sha384-", StringComparison.Ordinal) ||
                value.StartsWith("sha512-", StringComparison.Ordinal) ? 7 : 0;
            if (prefix == 0 || value.Length == prefix) return false;
            int padding = 0;
            foreach (char c in value.AsSpan(prefix))
            {
                if (c == '=') { if (++padding > 2) return false; }
                else if (padding != 0 || !(char.IsAsciiLetterOrDigit(c) || c is '+' or '/' or '-' or '_')) return false;
            }
            return true;
        }
        if (token.Contains('\'') || token.Contains('"') || token.Contains('\\') || token.Contains('?') || token.Contains('#')) return false;
        if (token == "*") return true;
        if (token.EndsWith(':') && !token.Contains('/'))
            return ValidScheme(token.AsSpan(0, token.Length - 1));
        // Parse a bounded host-source, including CSP wildcard hosts/ports.
        string host = token;
        int scheme = host.IndexOf("://", StringComparison.Ordinal);
        if (scheme >= 0)
        {
            if (!ValidScheme(host.AsSpan(0, scheme))) return false;
            host = host[(scheme + 3)..];
        }
        int slash = host.IndexOf('/');
        string authority = slash < 0 ? host : host[..slash];
        if (authority.Length == 0 || authority.Contains('@')) return false;
        int colon = authority.LastIndexOf(':');
        if (colon >= 0)
        {
            string port = authority[(colon + 1)..];
            if (port != "*" && (!int.TryParse(port, out int number) || number is < 0 or > 65_535)) return false;
            authority = authority[..colon];
        }
        if (authority.StartsWith("*.", StringComparison.Ordinal)) authority = authority[2..];
        if (authority.Length == 0 || !authority.All(c => char.IsAsciiLetterOrDigit(c) || c is '-' or '.')) return false;
        return slash < 0 || host.AsSpan(slash).IndexOfAny('\'', '"', '*') < 0;
    }

    private static bool ValidScheme(ReadOnlySpan<char> value)
    {
        if (value.IsEmpty || !char.IsAsciiLetter(value[0])) return false;
        foreach (char c in value)
            if (!(char.IsAsciiLetterOrDigit(c) || c is '+' or '-' or '.')) return false;
        return true;
    }
}