using AngleSharp.Dom;
namespace XFESystem.Browser;
/// <summary>A conservative nonce/strict-dynamic CSP profile. Unknown policies still fail closed.
/// eval, inline event attributes, modules and workers remain unavailable.</summary>
internal sealed class BrowserCspScriptPolicy
{
private readonly List<HashSet<string>> nonces = [];
private readonly HashSet<IElement> parserScripts;
private readonly bool declared, understood = true;
public BrowserCspScriptPolicy(string? header, IDocument document)
{
parserScripts = new(document.QuerySelectorAll("script"), ReferenceEqualityComparer.Instance);
var policies = new List<string>();
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<string>(StringComparer.OrdinalIgnoreCase);
HashSet<string>? 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("<script", StringComparison.OrdinalIgnoreCase) ||
attribute.Name.Contains("<style", StringComparison.OrdinalIgnoreCase) || attribute.Value.Contains("<script", StringComparison.OrdinalIgnoreCase) ||
attribute.Value.Contains("<style", StringComparison.OrdinalIgnoreCase));
return nonces.All(policy => 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;
}
using AngleSharp.Dom;
namespace XFESystem.Browser;
/// <summary>A conservative nonce/strict-dynamic CSP profile. Unknown policies still fail closed.
/// eval, inline event attributes, modules and workers remain unavailable.</summary>
internal sealed class BrowserCspScriptPolicy
{
private readonly List<HashSet<string>> nonces = [];
private readonly HashSet<IElement> parserScripts;
private readonly bool declared, understood = true;
public BrowserCspScriptPolicy(string? header, IDocument document)
{
parserScripts = new(document.QuerySelectorAll("script"), ReferenceEqualityComparer.Instance);
var policies = new List<string>();
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<string>(StringComparer.OrdinalIgnoreCase);
HashSet<string>? 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("<script", StringComparison.OrdinalIgnoreCase) ||
attribute.Name.Contains("<style", StringComparison.OrdinalIgnoreCase) || attribute.Value.Contains("<script", StringComparison.OrdinalIgnoreCase) ||
attribute.Value.Contains("<style", StringComparison.OrdinalIgnoreCase));
return nonces.All(policy => 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;
}