use std::collections::HashMap;
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Directive {
DefaultSrc,
ScriptSrc,
ScriptSrcElem,
ScriptSrcAttr,
StyleSrc,
StyleSrcElem,
StyleSrcAttr,
ImgSrc,
ConnectSrc,
FrameSrc,
ChildSrc,
FontSrc,
MediaSrc,
ObjectSrc,
WorkerSrc,
ManifestSrc,
PrefetchSrc,
}
impl Directive {
pub fn from_token(s: &str) -> Option<Self> {
Some(match s.to_ascii_lowercase().as_str() {
"default-src" => Directive::DefaultSrc,
"script-src" => Directive::ScriptSrc,
"script-src-elem" => Directive::ScriptSrcElem,
"script-src-attr" => Directive::ScriptSrcAttr,
"style-src" => Directive::StyleSrc,
"style-src-elem" => Directive::StyleSrcElem,
"style-src-attr" => Directive::StyleSrcAttr,
"img-src" => Directive::ImgSrc,
"connect-src" => Directive::ConnectSrc,
"frame-src" => Directive::FrameSrc,
"child-src" => Directive::ChildSrc,
"font-src" => Directive::FontSrc,
"media-src" => Directive::MediaSrc,
"object-src" => Directive::ObjectSrc,
"worker-src" => Directive::WorkerSrc,
"manifest-src" => Directive::ManifestSrc,
"prefetch-src" => Directive::PrefetchSrc,
_ => return None,
})
}
pub fn as_str(&self) -> &'static str {
match self {
Directive::DefaultSrc => "default-src",
Directive::ScriptSrc => "script-src",
Directive::ScriptSrcElem => "script-src-elem",
Directive::ScriptSrcAttr => "script-src-attr",
Directive::StyleSrc => "style-src",
Directive::StyleSrcElem => "style-src-elem",
Directive::StyleSrcAttr => "style-src-attr",
Directive::ImgSrc => "img-src",
Directive::ConnectSrc => "connect-src",
Directive::FrameSrc => "frame-src",
Directive::ChildSrc => "child-src",
Directive::FontSrc => "font-src",
Directive::MediaSrc => "media-src",
Directive::ObjectSrc => "object-src",
Directive::WorkerSrc => "worker-src",
Directive::ManifestSrc => "manifest-src",
Directive::PrefetchSrc => "prefetch-src",
}
}
pub fn fallback_chain(&self) -> &'static [Directive] {
use Directive::*;
match self {
ScriptSrcElem => &[ScriptSrcElem, ScriptSrc, DefaultSrc],
ScriptSrcAttr => &[ScriptSrcAttr, ScriptSrc, DefaultSrc],
ScriptSrc => &[ScriptSrc, DefaultSrc],
StyleSrcElem => &[StyleSrcElem, StyleSrc, DefaultSrc],
StyleSrcAttr => &[StyleSrcAttr, StyleSrc, DefaultSrc],
StyleSrc => &[StyleSrc, DefaultSrc],
FrameSrc => &[FrameSrc, ChildSrc, DefaultSrc],
ChildSrc => &[ChildSrc, DefaultSrc],
WorkerSrc => &[WorkerSrc, ChildSrc, ScriptSrc, DefaultSrc],
ImgSrc => &[ImgSrc, DefaultSrc],
ConnectSrc => &[ConnectSrc, DefaultSrc],
FontSrc => &[FontSrc, DefaultSrc],
MediaSrc => &[MediaSrc, DefaultSrc],
ObjectSrc => &[ObjectSrc, DefaultSrc],
ManifestSrc => &[ManifestSrc, DefaultSrc],
PrefetchSrc => &[PrefetchSrc, DefaultSrc],
DefaultSrc => &[DefaultSrc],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HashAlgo {
Sha256,
Sha384,
Sha512,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
All,
None_,
Self_,
UnsafeInline,
UnsafeEval,
UnsafeHashes,
StrictDynamic,
ReportSample,
Scheme(String),
Host(HostSource),
Nonce(String),
Hash(HashAlgo, String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostSource {
pub scheme: Option<String>, pub host: HostPattern,
pub port: Option<PortPattern>,
pub path: Option<String>, }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostPattern {
Wildcard(String), Exact(String), }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PortPattern {
Wildcard, Exact(u16),
}
#[derive(Debug, Clone, Default)]
pub struct Policy {
pub directives: HashMap<Directive, Vec<Source>>,
pub report_only: bool,
}
#[derive(Debug, Clone, Default)]
pub struct PolicySet {
pub policies: Vec<Policy>,
}
impl PolicySet {
pub fn is_empty(&self) -> bool {
self.policies.iter().all(|p| p.directives.is_empty())
}
pub fn push_header(&mut self, value: &str, report_only: bool) {
for piece in split_top_level(value, ',') {
let policy = Policy::parse_serialized(piece, report_only);
if !policy.directives.is_empty() {
self.policies.push(policy);
}
}
}
pub fn push_meta(&mut self, content: &str) {
for piece in split_top_level(content, ',') {
let policy = Policy::parse_serialized(piece, false);
if !policy.directives.is_empty() {
self.policies.push(policy);
}
}
}
}
fn split_top_level(s: &str, delim: char) -> impl Iterator<Item = &str> {
s.split(delim).map(str::trim).filter(|p| !p.is_empty())
}
impl Policy {
pub fn parse_serialized(s: &str, report_only: bool) -> Policy {
let mut policy = Policy {
directives: HashMap::new(),
report_only,
};
for raw in s.split(';') {
let raw = raw.trim();
if raw.is_empty() {
continue;
}
let mut tokens = raw.split_ascii_whitespace();
let dir_token = match tokens.next() {
Some(t) => t,
None => continue,
};
let Some(directive) = Directive::from_token(dir_token) else {
continue;
};
let mut sources = Vec::new();
for tok in tokens {
if let Some(src) = Source::parse(tok) {
sources.push(src);
}
}
policy
.directives
.entry(directive)
.or_default()
.extend(sources);
}
policy
}
pub fn parse_header(s: &str) -> PolicySet {
let mut set = PolicySet::default();
set.push_header(s, false);
set
}
pub fn parse_meta_content(s: &str) -> PolicySet {
let mut set = PolicySet::default();
set.push_meta(s);
set
}
}
impl Source {
fn parse(token: &str) -> Option<Source> {
let lc = token.to_ascii_lowercase();
match lc.as_str() {
"*" => return Some(Source::All),
"'none'" => return Some(Source::None_),
"'self'" => return Some(Source::Self_),
"'unsafe-inline'" => return Some(Source::UnsafeInline),
"'unsafe-eval'" => return Some(Source::UnsafeEval),
"'unsafe-hashes'" => return Some(Source::UnsafeHashes),
"'strict-dynamic'" => return Some(Source::StrictDynamic),
"'report-sample'" => return Some(Source::ReportSample),
_ => {}
}
if let Some(rest) = token.strip_prefix("'nonce-") {
if let Some(value) = rest.strip_suffix('\'') {
return Some(Source::Nonce(value.to_string()));
}
}
for (algo, prefix) in [
(HashAlgo::Sha256, "'sha256-"),
(HashAlgo::Sha384, "'sha384-"),
(HashAlgo::Sha512, "'sha512-"),
] {
if let Some(rest) = token.strip_prefix(prefix) {
if let Some(value) = rest.strip_suffix('\'') {
return Some(Source::Hash(algo, value.to_string()));
}
}
}
if let Some(scheme) = token.strip_suffix(':') {
if !scheme.contains('/')
&& scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
{
return Some(Source::Scheme(scheme.to_ascii_lowercase()));
}
}
if !token.starts_with('\'') {
return parse_host_source(token).map(Source::Host);
}
None
}
}
fn parse_host_source(token: &str) -> Option<HostSource> {
let mut rest = token;
let mut scheme = None;
if let Some(idx) = rest.find("://") {
let s = rest[..idx].to_ascii_lowercase();
if s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
&& !s.is_empty()
{
scheme = Some(s);
rest = &rest[idx + 3..];
}
}
let (host_port, path) = match rest.find('/') {
Some(idx) => (&rest[..idx], Some(rest[idx..].to_string())),
None => (rest, None),
};
if host_port.is_empty() {
return None;
}
let (host_part, port) = match host_port.rfind(':') {
Some(idx) => {
let port_str = &host_port[idx + 1..];
if port_str == "*" {
(&host_port[..idx], Some(PortPattern::Wildcard))
} else if let Ok(n) = port_str.parse::<u16>() {
(&host_port[..idx], Some(PortPattern::Exact(n)))
} else {
(host_port, None)
}
}
None => (host_port, None),
};
let host = if host_part == "*" {
HostPattern::Exact("*".to_string())
} else if let Some(suffix) = host_part.strip_prefix("*.") {
HostPattern::Wildcard(suffix.to_ascii_lowercase())
} else {
if host_part.is_empty()
|| host_part.contains('*')
|| !host_part
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
{
return None;
}
HostPattern::Exact(host_part.to_ascii_lowercase())
};
Some(HostSource {
scheme,
host,
port,
path,
})
}
#[derive(Debug, Clone)]
pub struct CheckCtx<'a> {
pub directive: Directive,
pub url: &'a Url,
pub page_origin: &'a Url,
pub nonce: Option<&'a str>,
pub parser_inserted: bool,
}
#[derive(Debug, Clone)]
pub struct AllowDecision {
pub allowed: bool,
pub matched_directive: Directive,
pub report_only: bool,
}
impl AllowDecision {
pub fn allow_no_policy() -> Self {
Self {
allowed: true,
matched_directive: Directive::DefaultSrc,
report_only: false,
}
}
}
impl PolicySet {
pub fn allows(&self, ctx: &CheckCtx<'_>) -> AllowDecision {
if self.policies.is_empty() {
return AllowDecision::allow_no_policy();
}
for policy in &self.policies {
let decision = policy.allows(ctx);
if !decision.allowed && !policy.report_only {
return decision;
}
}
AllowDecision::allow_no_policy()
}
}
impl Policy {
pub fn allows(&self, ctx: &CheckCtx<'_>) -> AllowDecision {
for &candidate in ctx.directive.fallback_chain() {
if let Some(sources) = self.directives.get(&candidate) {
let allowed = match_sources(sources, ctx);
return AllowDecision {
allowed,
matched_directive: candidate,
report_only: self.report_only,
};
}
}
AllowDecision::allow_no_policy()
}
}
fn match_sources(sources: &[Source], ctx: &CheckCtx<'_>) -> bool {
if sources.is_empty() {
return false;
}
if sources.iter().any(|s| matches!(s, Source::None_)) && sources.len() == 1 {
return false;
}
let strict_dynamic = is_script_directive(ctx.directive)
&& sources.iter().any(|s| matches!(s, Source::StrictDynamic));
for src in sources {
match src {
Source::None_
| Source::UnsafeInline
| Source::UnsafeEval
| Source::UnsafeHashes
| Source::ReportSample
| Source::StrictDynamic => continue,
Source::All if !strict_dynamic => {
if is_network_scheme(ctx.url.scheme()) {
return true;
}
}
Source::All => continue,
Source::Self_ if !strict_dynamic => {
if is_same_origin(ctx.url, ctx.page_origin) {
return true;
}
}
Source::Self_ => continue,
Source::Scheme(s) if !strict_dynamic => {
if ctx.url.scheme().eq_ignore_ascii_case(s) {
return true;
}
}
Source::Scheme(_) => continue,
Source::Host(h) if !strict_dynamic => {
if host_source_matches(h, ctx.url) {
return true;
}
}
Source::Host(_) => continue,
Source::Nonce(token) => {
if let Some(supplied) = ctx.nonce {
if supplied == token {
return true;
}
}
}
Source::Hash(_, _) => {
continue;
}
}
}
false
}
fn is_script_directive(d: Directive) -> bool {
matches!(
d,
Directive::ScriptSrc | Directive::ScriptSrcElem | Directive::ScriptSrcAttr
)
}
fn is_network_scheme(scheme: &str) -> bool {
matches!(scheme, "http" | "https" | "ws" | "wss" | "ftp" | "ftps")
}
fn is_same_origin(a: &Url, b: &Url) -> bool {
a.scheme() == b.scheme()
&& a.host_str() == b.host_str()
&& a.port_or_known_default() == b.port_or_known_default()
}
fn host_source_matches(src: &HostSource, url: &Url) -> bool {
if let Some(want) = &src.scheme {
if !url.scheme().eq_ignore_ascii_case(want) {
return false;
}
} else {
if !is_network_scheme(url.scheme()) {
return false;
}
}
let url_host = match url.host_str() {
Some(h) => h.to_ascii_lowercase(),
None => return false,
};
let host_ok = match &src.host {
HostPattern::Exact(want) => want == "*" || want == &url_host,
HostPattern::Wildcard(suffix) => {
url_host.ends_with(suffix)
&& url_host.len() > suffix.len()
&& url_host.chars().nth(url_host.len() - suffix.len() - 1) == Some('.')
}
};
if !host_ok {
return false;
}
let url_port = url.port_or_known_default();
if let Some(p) = &src.port {
match p {
PortPattern::Wildcard => {} PortPattern::Exact(n) => {
if url_port != Some(*n) {
return false;
}
}
}
} else {
let default_port = match url.scheme() {
"http" | "ws" | "ftp" => Some(80),
"https" | "wss" | "ftps" => Some(443),
_ => None,
};
if url_port != default_port {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
const WALMART_CSP: &str = "child-src 'self' blob:; \
connect-src 'self' *.akamaihd.net *.perimeterx.net; \
script-src 'self' 'strict-dynamic' 'nonce-MRjHHgrLk9lNoNBv' *.walmartimages.com; \
style-src 'self' 'unsafe-inline' *.walmartimages.com; \
img-src 'self' data: *.walmartimages.com *.scene7.com; \
frame-src 'self' *.youtube.com";
#[test]
fn parses_retailer_csp_directives() {
let set = Policy::parse_meta_content(WALMART_CSP);
assert_eq!(set.policies.len(), 1);
let p = &set.policies[0];
assert!(!p.report_only);
assert!(p.directives.contains_key(&Directive::ScriptSrc));
assert!(p.directives.contains_key(&Directive::ConnectSrc));
assert!(p.directives.contains_key(&Directive::FrameSrc));
}
#[test]
fn parses_strict_dynamic_and_nonce() {
let set = Policy::parse_meta_content(WALMART_CSP);
let script_src = &set.policies[0].directives[&Directive::ScriptSrc];
assert!(
script_src
.iter()
.any(|s| matches!(s, Source::StrictDynamic)),
"must parse 'strict-dynamic' keyword"
);
assert!(
script_src
.iter()
.any(|s| matches!(s, Source::Nonce(n) if n == "MRjHHgrLk9lNoNBv")),
"must parse 'nonce-...'"
);
assert!(
script_src.iter().any(|s| matches!(s, Source::Self_)),
"must parse 'self'"
);
}
#[test]
fn parses_host_source_with_subdomain_wildcard() {
let set = Policy::parse_meta_content("connect-src *.example.com:8443");
let cs = &set.policies[0].directives[&Directive::ConnectSrc];
assert_eq!(cs.len(), 1);
let Source::Host(h) = &cs[0] else {
panic!("expected host source")
};
assert_eq!(h.host, HostPattern::Wildcard("example.com".to_string()));
assert_eq!(h.port, Some(PortPattern::Exact(8443)));
}
#[test]
fn parses_scheme_only_source() {
let set = Policy::parse_meta_content("img-src data: blob: https:");
let img = &set.policies[0].directives[&Directive::ImgSrc];
assert_eq!(img.len(), 3);
assert!(img
.iter()
.any(|s| matches!(s, Source::Scheme(x) if x == "data")));
assert!(img
.iter()
.any(|s| matches!(s, Source::Scheme(x) if x == "blob")));
assert!(img
.iter()
.any(|s| matches!(s, Source::Scheme(x) if x == "https")));
}
#[test]
fn parses_hash_sources() {
let set =
Policy::parse_meta_content("script-src 'sha256-abc123==' 'sha384-XYZ' 'sha512-q+w'");
let ss = &set.policies[0].directives[&Directive::ScriptSrc];
assert_eq!(ss.len(), 3);
assert!(matches!(&ss[0], Source::Hash(HashAlgo::Sha256, h) if h == "abc123=="));
assert!(matches!(&ss[1], Source::Hash(HashAlgo::Sha384, h) if h == "XYZ"));
assert!(matches!(&ss[2], Source::Hash(HashAlgo::Sha512, h) if h == "q+w"));
}
#[test]
fn parses_none_keyword() {
let set = Policy::parse_meta_content("object-src 'none'");
let os = &set.policies[0].directives[&Directive::ObjectSrc];
assert_eq!(os.len(), 1);
assert!(matches!(&os[0], Source::None_));
}
#[test]
fn parses_multiple_policies_from_one_header() {
let mut set = PolicySet::default();
set.push_header("script-src 'self', script-src https:", false);
assert_eq!(set.policies.len(), 2);
}
#[test]
fn report_only_flag_propagates() {
let mut set = PolicySet::default();
set.push_header("script-src 'self'", true);
assert!(set.policies[0].report_only);
}
#[test]
fn unknown_directive_is_dropped_silently() {
let set = Policy::parse_meta_content("script-src 'self'; bogus-thing 'self'");
assert_eq!(set.policies[0].directives.len(), 1);
}
#[test]
fn fallback_chain_for_script_src_elem_includes_default() {
let chain = Directive::ScriptSrcElem.fallback_chain();
assert_eq!(
chain,
&[
Directive::ScriptSrcElem,
Directive::ScriptSrc,
Directive::DefaultSrc
]
);
}
#[test]
fn fallback_chain_for_frame_src_includes_child_then_default() {
let chain = Directive::FrameSrc.fallback_chain();
assert_eq!(
chain,
&[
Directive::FrameSrc,
Directive::ChildSrc,
Directive::DefaultSrc
]
);
}
#[test]
fn parses_bare_star_host() {
let set = Policy::parse_meta_content("img-src *");
let img = &set.policies[0].directives[&Directive::ImgSrc];
assert!(matches!(&img[0], Source::All));
}
#[test]
fn parses_directive_with_no_sources_as_block_all() {
let set = Policy::parse_meta_content("script-src");
let ss = &set.policies[0].directives[&Directive::ScriptSrc];
assert_eq!(ss.len(), 0);
}
fn url(s: &str) -> Url {
Url::parse(s).unwrap()
}
fn ctx<'a>(
directive: Directive,
u: &'a Url,
origin: &'a Url,
nonce: Option<&'a str>,
parser_inserted: bool,
) -> CheckCtx<'a> {
CheckCtx {
directive,
url: u,
page_origin: origin,
nonce,
parser_inserted,
}
}
#[test]
fn empty_policy_set_allows_everything() {
let set = PolicySet::default();
let u = url("https://akamai.com/sensor.js");
let origin = url("https://www.walmart.com/");
let d = set.allows(&ctx(Directive::ScriptSrcElem, &u, &origin, None, true));
assert!(d.allowed);
}
#[test]
fn self_matches_same_origin() {
let set = Policy::parse_meta_content("script-src 'self'");
let origin = url("https://example.com/");
let same = url("https://example.com/app.js");
let other = url("https://other.com/x.js");
assert!(
set.allows(&ctx(Directive::ScriptSrcElem, &same, &origin, None, true))
.allowed
);
assert!(
!set.allows(&ctx(Directive::ScriptSrcElem, &other, &origin, None, true))
.allowed
);
}
#[test]
fn host_wildcard_matches_subdomain_only() {
let set = Policy::parse_meta_content("img-src *.example.com");
let origin = url("https://example.com/");
let sub = url("https://images.example.com/a.png");
let bare = url("https://example.com/a.png");
assert!(
set.allows(&ctx(Directive::ImgSrc, &sub, &origin, None, false))
.allowed
);
assert!(
!set.allows(&ctx(Directive::ImgSrc, &bare, &origin, None, false))
.allowed
);
}
#[test]
fn scheme_only_source_matches_any_host_on_that_scheme() {
let set = Policy::parse_meta_content("img-src data: https:");
let origin = url("https://example.com/");
let data = url("data:image/png;base64,iVBORw0K");
let any_https = url("https://random.cdn.net/x.png");
let http = url("http://random.cdn.net/x.png");
assert!(
set.allows(&ctx(Directive::ImgSrc, &data, &origin, None, false))
.allowed
);
assert!(
set.allows(&ctx(Directive::ImgSrc, &any_https, &origin, None, false))
.allowed
);
assert!(
!set.allows(&ctx(Directive::ImgSrc, &http, &origin, None, false))
.allowed
);
}
#[test]
fn none_blocks_everything() {
let set = Policy::parse_meta_content("object-src 'none'");
let origin = url("https://example.com/");
let any = url("https://example.com/x.swf");
assert!(
!set.allows(&ctx(Directive::ObjectSrc, &any, &origin, None, false))
.allowed
);
}
#[test]
fn fallback_chain_uses_default_src_when_specific_missing() {
let set = Policy::parse_meta_content("default-src 'self'");
let origin = url("https://example.com/");
let self_url = url("https://example.com/x.png");
let other = url("https://other.com/x.png");
assert!(
set.allows(&ctx(Directive::ImgSrc, &self_url, &origin, None, false))
.allowed
);
assert!(
!set.allows(&ctx(Directive::ImgSrc, &other, &origin, None, false))
.allowed
);
}
#[test]
fn nonce_authorizes_under_normal_policy() {
let set = Policy::parse_meta_content("script-src 'nonce-abc123'");
let origin = url("https://example.com/");
let any = url("https://cdn.elsewhere.com/app.js");
assert!(
set.allows(&ctx(
Directive::ScriptSrcElem,
&any,
&origin,
Some("abc123"),
true
))
.allowed
);
assert!(
!set.allows(&ctx(
Directive::ScriptSrcElem,
&any,
&origin,
Some("WRONG"),
true
))
.allowed
);
assert!(
!set.allows(&ctx(Directive::ScriptSrcElem, &any, &origin, None, true))
.allowed
);
}
#[test]
fn strict_dynamic_blocks_parser_injected_sensor_script() {
let set = Policy::parse_meta_content(WALMART_CSP);
let origin = url("https://www.walmart.com/");
let injected_script = url("https://www.walmart.com/akam/13/3e35295b");
let d = set.allows(&ctx(
Directive::ScriptSrcElem,
&injected_script,
&origin,
None,
true,
));
assert!(
!d.allowed,
"Akamai parser-injected script must be blocked under strict-dynamic"
);
assert_eq!(d.matched_directive, Directive::ScriptSrc);
let d = set.allows(&ctx(
Directive::ScriptSrcElem,
&injected_script,
&origin,
Some("MRjHHgrLk9lNoNBv"),
true,
));
assert!(d.allowed, "matching nonce overrides strict-dynamic block");
}
#[test]
fn strict_dynamic_ignores_self_and_host_allowlist() {
let set = Policy::parse_meta_content(WALMART_CSP);
let origin = url("https://www.walmart.com/");
let images = url("https://i5.walmartimages.com/foo.js");
assert!(
!set.allows(&ctx(Directive::ScriptSrcElem, &images, &origin, None, true))
.allowed
);
assert!(
set.allows(&ctx(
Directive::ScriptSrcElem,
&images,
&origin,
Some("MRjHHgrLk9lNoNBv"),
true,
))
.allowed
);
}
#[test]
fn strict_dynamic_does_not_apply_to_non_script_directives() {
let set = Policy::parse_meta_content(WALMART_CSP);
let origin = url("https://www.walmart.com/");
let img = url("https://i5.walmartimages.com/foo.png");
assert!(
set.allows(&ctx(Directive::ImgSrc, &img, &origin, None, false))
.allowed
);
}
#[test]
fn host_with_wildcard_port_matches_any_port() {
let set = Policy::parse_meta_content("connect-src example.com:*");
let origin = url("https://other.com/");
let p443 = url("https://example.com/x");
let p8443 = url("https://example.com:8443/x");
assert!(
set.allows(&ctx(Directive::ConnectSrc, &p443, &origin, None, false))
.allowed
);
assert!(
set.allows(&ctx(Directive::ConnectSrc, &p8443, &origin, None, false))
.allowed
);
}
#[test]
fn host_without_port_matches_only_default_port() {
let set = Policy::parse_meta_content("connect-src example.com");
let origin = url("https://other.com/");
let p443 = url("https://example.com/x"); let p8443 = url("https://example.com:8443/x"); assert!(
set.allows(&ctx(Directive::ConnectSrc, &p443, &origin, None, false))
.allowed
);
assert!(
!set.allows(&ctx(Directive::ConnectSrc, &p8443, &origin, None, false))
.allowed
);
}
#[test]
fn report_only_policy_never_blocks() {
let mut set = PolicySet::default();
set.push_header("script-src 'none'", true);
let origin = url("https://example.com/");
let any = url("https://example.com/x.js");
assert!(
set.allows(&ctx(Directive::ScriptSrcElem, &any, &origin, None, true))
.allowed
);
}
#[test]
fn multiple_policies_intersect_most_restrictive() {
let mut set = PolicySet::default();
set.push_header("script-src 'self' https://cdn.com", false);
set.push_header("script-src 'self'", false); let origin = url("https://example.com/");
let cdn = url("https://cdn.com/x.js");
let self_url = url("https://example.com/x.js");
assert!(
!set.allows(&ctx(Directive::ScriptSrcElem, &cdn, &origin, None, true))
.allowed
);
assert!(
set.allows(&ctx(
Directive::ScriptSrcElem,
&self_url,
&origin,
None,
true
))
.allowed
);
}
}