using AngleSharp.Dom; namespace XFESystem.Browser; /// A conservative nonce/strict-dynamic CSP profile. Unknown policies still fail closed. /// eval, inline event attributes, modules and workers remain unavailable. internal sealed class BrowserCspScriptPolicy { private readonly List> nonces = []; private readonly HashSet parserScripts; private readonly bool declared, understood = true; public BrowserCspScriptPolicy(string? header, IDocument document) { parserScripts = new(document.QuerySelectorAll("script"), ReferenceEqualityComparer.Instance); var policies = new List(); if (header is not null) policies.AddRange(header.Split(',')); foreach (var meta in document.QuerySelectorAll("meta[http-equiv]")) if (meta.GetAttribute("http-equiv")!.Equals("Content-Security-Policy", StringComparison.OrdinalIgnoreCase)) policies.Add(meta.GetAttribute("content") ?? ""); declared = policies.Count != 0; foreach (string policy in policies) { if (policy.Length is 0 or > 65_536 || policy.Any(c => c != '\t' && (c < ' ' || c > '~'))) { understood = false; continue; } var seen = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet? sources = null; foreach (string directive in policy.Split(';')) { var tokens = directive.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries); if (tokens.Length == 0 || !seen.Add(tokens[0])) continue; // CSP uses the first occurrence. switch (tokens[0].ToLowerInvariant()) { case "script-src": sources = new(StringComparer.Ordinal); bool strict = tokens.Contains("'strict-dynamic'"); foreach (string token in tokens.Skip(1)) { if (token.StartsWith("'nonce-", StringComparison.Ordinal) && token.EndsWith('\'')) { string nonce = token[7..^1]; string unpadded = nonce.TrimEnd('='); if (unpadded.Length != 0 && nonce.Length - unpadded.Length <= 2 && unpadded.All(c => char.IsAsciiLetterOrDigit(c) || c is '+' or '/' or '-' or '_')) sources.Add(nonce); else understood = false; } else if (token is not ("https:" or "'strict-dynamic'" or "'report-sample'" or "'wasm-unsafe-eval'")) understood = false; } if (!strict || sources.Count == 0) understood = false; break; case "base-uri": if (tokens.Length != 2 || tokens[1] != "'self'") understood = false; break; case "report-to": if (tokens.Length != 2 || !tokens[1].All(c => char.IsAsciiLetterOrDigit(c) || c is '-' or '_' or '.')) understood = false; break; default: understood = false; break; } } if (sources is null) understood = false; else nonces.Add(sources); } } internal bool BlocksAllScripts => declared && !understood; internal bool Allows(IElement script, bool createdByApi) { if (!declared) return true; if (!understood) return false; // Only scripts created through the executing realm's DOM API enter the // dynamic queue. innerHTML/fragment scripts remain inert in DomBridge. bool dynamic = !parserScripts.Contains(script) && createdByApi; bool nonceable = !script.Attributes.Any(attribute => attribute.Name.Contains(" nonceable && policy.Contains(script.GetAttribute("nonce") ?? "") || dynamic); } internal bool AllowsBase(Uri document, Uri candidate) => !declared || understood && document.Scheme == candidate.Scheme && document.IdnHost == candidate.IdnHost && document.Port == candidate.Port; }