use csp_parse::{
DirectiveStatus, DirectiveValue, HashAlgorithm, HostPart, Keyword, SourceExpression,
SourceList, ancestor_source_list_is_valid, parse_policy_list, registry_lookup,
};
#[test]
fn spec_style_multi_directive_fetch_policy() {
let list = parse_policy_list(
"default-src 'self'; img-src *; \
object-src media1.example.com media2.example.com *.cdn.example.com; \
script-src trustedscripts.example.com",
);
let directives = &list.policies[0].directives;
assert_eq!(
directives
.iter()
.map(|d| d.name.as_str())
.collect::<Vec<_>>(),
vec!["default-src", "img-src", "object-src", "script-src"]
);
match directives[0].value() {
DirectiveValue::SourceList(SourceList::Sources(entries)) => {
assert_eq!(
entries[0].expression,
Some(SourceExpression::Keyword(Keyword::SelfKeyword))
);
}
other => panic!("unexpected default-src value: {other:?}"),
}
match directives[1].value() {
DirectiveValue::SourceList(SourceList::Sources(entries)) => match &entries[0].expression {
Some(SourceExpression::Host(host)) => {
assert_eq!(host.scheme, None);
assert_eq!(host.host, HostPart::AnyHost);
assert_eq!(host.port, None);
assert_eq!(host.path, None);
}
other => panic!("unexpected img-src entry: {other:?}"),
},
other => panic!("unexpected img-src value: {other:?}"),
}
match directives[2].value() {
DirectiveValue::SourceList(SourceList::Sources(entries)) => {
assert_eq!(entries.len(), 3);
assert!(entries.iter().all(|e| e.expression.is_some()));
}
other => panic!("unexpected object-src value: {other:?}"),
}
}
#[test]
fn nonce_and_hash_source_script_src() {
let list = parse_policy_list(
"script-src 'nonce-2726c7f26c' 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='",
);
match &list.policies[0].directives[0].value() {
DirectiveValue::SourceList(SourceList::Sources(entries)) => {
assert_eq!(
entries[0].expression,
Some(SourceExpression::Nonce("2726c7f26c".to_string()))
);
match &entries[1].expression {
Some(SourceExpression::Hash(hash)) => {
assert_eq!(hash.algorithm, HashAlgorithm::Sha256);
assert_eq!(hash.value, "B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=");
}
other => panic!("unexpected second entry: {other:?}"),
}
}
other => panic!("unexpected script-src value: {other:?}"),
}
}
#[test]
fn reporting_directives() {
let list = parse_policy_list("default-src 'self'; report-uri /csp-report-endpoint");
assert_eq!(
list.policies[0].directives[1].value(),
DirectiveValue::TokenList(vec!["/csp-report-endpoint".to_string()])
);
assert_eq!(
registry_lookup("report-uri").map(|(_, status)| status),
Some(DirectiveStatus::Deprecated)
);
let list = parse_policy_list("default-src 'self'; report-to csp-endpoint");
assert_eq!(
list.policies[0].directives[1].value(),
DirectiveValue::Token(Some("csp-endpoint".to_string()))
);
}
#[test]
fn frame_ancestors_realistic_allowlist() {
let list = parse_policy_list("frame-ancestors 'self' https://example.com");
match list.policies[0].directives[0].value() {
DirectiveValue::AncestorSourceList(source_list) => {
assert!(ancestor_source_list_is_valid(&source_list));
}
other => panic!("unexpected frame-ancestors value: {other:?}"),
}
}
#[test]
fn upgrade_insecure_requests_with_scheme_source() {
let list = parse_policy_list("upgrade-insecure-requests; default-src https:");
assert_eq!(
list.policies[0].directives[0].value(),
DirectiveValue::Boolean
);
match list.policies[0].directives[1].value() {
DirectiveValue::SourceList(SourceList::Sources(entries)) => {
assert_eq!(
entries[0].expression,
Some(SourceExpression::Scheme("https".to_string()))
);
}
other => panic!("unexpected default-src value: {other:?}"),
}
}
#[test]
fn trusted_types_and_sandbox() {
let list = parse_policy_list("require-trusted-types-for 'script'; trusted-types my-policy");
assert_eq!(
list.policies[0].directives[0].value(),
DirectiveValue::Token(Some("'script'".to_string()))
);
assert_eq!(
list.policies[0].directives[1].value(),
DirectiveValue::TrustedTypes(vec!["my-policy".to_string()])
);
let list = parse_policy_list("sandbox allow-forms allow-scripts");
assert_eq!(
list.policies[0].directives[0].value(),
DirectiveValue::Sandbox(vec!["allow-forms".to_string(), "allow-scripts".to_string()])
);
}
#[test]
fn comma_joined_policies_parse_independently() {
let list = parse_policy_list("default-src 'self' 'unsafe-inline', default-src 'none'");
assert_eq!(list.policies.len(), 2);
assert!(matches!(
list.policies[0].directives[0].value(),
DirectiveValue::SourceList(SourceList::Sources(_))
));
assert_eq!(
list.policies[1].directives[0].value(),
DirectiveValue::SourceList(SourceList::None)
);
}
#[test]
fn host_with_trailing_dot() {
let list = parse_policy_list("script-src example.com.");
match &list.policies[0].directives[0].value() {
DirectiveValue::SourceList(SourceList::Sources(entries)) => match &entries[0].expression {
Some(SourceExpression::Host(host)) => {
assert_eq!(
host.host,
HostPart::Named {
wildcard_prefix: false,
labels: vec!["example".to_string(), "com".to_string()],
trailing_dot: true,
}
);
}
other => panic!("unexpected entry: {other:?}"),
},
other => panic!("unexpected script-src value: {other:?}"),
}
}
#[test]
fn real_world_shaped_robustness_case() {
let list = parse_policy_list(
"default-src 'self'; \
script-src 'self' 'strict-dynamic' 'nonce-abc123' https://cdn.example.com; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data: https:; \
connect-src 'self' https://api.example.com wss://ws.example.com; \
frame-ancestors 'none'; \
base-uri 'self'; \
object-src 'none'; \
upgrade-insecure-requests; \
report-to csp-endpoint; \
report-uri /csp-report-endpoint",
);
let directives = &list.policies[0].directives;
assert_eq!(directives.len(), 11);
for directive in directives {
assert_ne!(
directive.value(),
DirectiveValue::Unknown,
"{} unexpectedly unregistered",
directive.name
);
}
}
#[test]
fn unknown_valueless_directive_does_not_affect_neighboring_directive() {
let list = parse_policy_list("img-src 'none'; aaa;");
let directives = &list.policies[0].directives;
assert_eq!(
directives[0].value(),
DirectiveValue::SourceList(SourceList::None)
);
assert_eq!(directives[1].name, "aaa");
assert_eq!(directives[1].value(), DirectiveValue::Unknown);
}