use browser_oxide::stealth::presets::chrome_148_macos;
use browser_oxide::Page;
const HTML: &str = r#"<!doctype html>
<html><head>
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'strict-dynamic' 'nonce-MRjHHgrLk9lNoNBv'">
<title>csp test</title>
</head><body>
<script nonce="MRjHHgrLk9lNoNBv">
globalThis.__legitimate_inline_ran = true;
</script>
<!--
Parser-injected without a nonce — must be blocked under strict-dynamic.
We point it at a never-resolvable host so a fetch attempt would surface
as a network error in the runtime; if the engine respects CSP, the
fetch is never attempted at all and the page reaches DOMContentLoaded
cleanly.
-->
<script src="https://blocked-by-csp.invalid./payload.js"></script>
</body></html>"#;
#[tokio::test]
async fn parser_injected_script_without_nonce_is_blocked() {
use std::sync::{Arc, Mutex};
use browser_oxide::csp_collector::collect_csp;
use browser_oxide::html_parser::parse_html;
use browser_oxide::js_runtime::extensions::fetch_ext as csp_state;
use browser_oxide::net::csp::Directive;
use url::Url;
let dom = parse_html(HTML);
let policy = collect_csp(&[], &dom);
csp_state::set_csp_policy(
Arc::new(policy),
Url::parse("https://example.com/").unwrap(),
true,
);
let blocked_url = Url::parse("https://blocked-by-csp.invalid./payload.js").unwrap();
let block_decision = csp_state::check_csp(
Directive::ScriptSrcElem,
&blocked_url,
None, true, );
assert!(
block_decision.is_err(),
"parser-injected, no-nonce script must trip CSP"
);
assert_eq!(block_decision.unwrap_err(), "script-src");
let allowed_decision = csp_state::check_csp(
Directive::ScriptSrcElem,
&blocked_url,
Some("MRjHHgrLk9lNoNBv"),
true,
);
assert!(
allowed_decision.is_ok(),
"matching nonce must clear CSP under strict-dynamic"
);
let captured_log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let _captured_for_log = captured_log.clone();
let mut page = Page::from_html(HTML, Some(chrome_148_macos()))
.await
.unwrap();
let inline_ran = page
.evaluate("String(globalThis.__legitimate_inline_ran)")
.unwrap();
assert_eq!(
inline_ran, "true",
"inline script with matching nonce must execute"
);
csp_state::clear_csp_policy();
}
#[tokio::test]
async fn no_csp_does_not_block_anything() {
const NO_CSP_HTML: &str = r#"<!doctype html>
<html><head><title>no csp</title></head><body>
<script>globalThis.__inline_ran = "yes";</script>
</body></html>"#;
let mut page = Page::from_html(NO_CSP_HTML, Some(chrome_148_macos()))
.await
.unwrap();
let inline = page.evaluate("globalThis.__inline_ran").unwrap();
assert_eq!(inline.trim_matches('"'), "yes");
}
#[tokio::test]
#[ignore = "not yet implemented: CSP violation event delivery to document listeners"]
async fn securitypolicyviolation_event_fires_on_block() {
use browser_oxide::js_runtime::extensions::fetch_ext as csp_state;
use browser_oxide::net::csp::Directive;
use url::Url;
const HTML: &str = r#"<html><head>
<meta http-equiv="Content-Security-Policy" content="connect-src 'self'">
<title>spv</title>
</head><body>
<script>
globalThis.__spv_events = [];
document.addEventListener('securitypolicyviolation', (e) => {
globalThis.__spv_events.push({
blockedURI: e.blockedURI,
effectiveDirective: e.effectiveDirective,
violatedDirective: e.violatedDirective,
disposition: e.disposition,
typeOk: typeof SecurityPolicyViolationEvent === 'function' && (e instanceof SecurityPolicyViolationEvent),
});
});
</script>
</body></html>"#;
let mut page = Page::from_html(HTML, Some(chrome_148_macos()))
.await
.unwrap();
let _ = csp_state::check_csp(
Directive::ConnectSrc,
&Url::parse("https://collector.example/api").unwrap(),
None,
false,
);
let _ = page
.evaluate("globalThis.__drainCspViolations && globalThis.__drainCspViolations()")
.unwrap();
let n = page
.evaluate("String(globalThis.__spv_events.length)")
.unwrap();
let n_clean = n.trim_matches('"');
assert!(
n_clean.parse::<i64>().unwrap_or(0) >= 1,
"at least one securitypolicyviolation event must have fired, got {n_clean}"
);
let blocked = page
.evaluate("globalThis.__spv_events[0].blockedURI")
.unwrap();
assert!(
blocked.contains("collector.example"),
"blockedURI must point at the blocked URL, got {blocked}"
);
let directive = page
.evaluate("globalThis.__spv_events[0].effectiveDirective")
.unwrap();
assert!(
directive.contains("connect-src"),
"effectiveDirective must echo 'connect-src', got {directive}"
);
let type_ok = page
.evaluate("String(globalThis.__spv_events[0].typeOk)")
.unwrap();
assert_eq!(
type_ok, "true",
"the event must be a SecurityPolicyViolationEvent instance"
);
let disposition = page
.evaluate("globalThis.__spv_events[0].disposition")
.unwrap();
assert!(
disposition.contains("enforce"),
"disposition must be 'enforce' for this policy, got {disposition}"
);
csp_state::clear_csp_policy();
}
#[tokio::test]
async fn connect_src_blocks_disallowed_hosts() {
use browser_oxide::csp_collector::collect_csp;
use browser_oxide::html_parser::parse_html;
use browser_oxide::js_runtime::extensions::fetch_ext as csp_state;
use browser_oxide::net::csp::Directive;
use std::sync::Arc;
use url::Url;
const CSP_HTML: &str = r#"<!doctype html><html><head>
<meta http-equiv="Content-Security-Policy"
content="connect-src 'self' https://api.example.com">
</head><body></body></html>"#;
let dom = parse_html(CSP_HTML);
let policy = collect_csp(&[], &dom);
csp_state::set_csp_policy(
Arc::new(policy),
Url::parse("https://example.com/").unwrap(),
true,
);
let allowed = csp_state::check_csp(
Directive::ConnectSrc,
&Url::parse("https://example.com/api/v1/data").unwrap(),
None,
false,
);
assert!(allowed.is_ok(), "same-origin connect must be allowed");
let api = csp_state::check_csp(
Directive::ConnectSrc,
&Url::parse("https://api.example.com/feed").unwrap(),
None,
false,
);
assert!(api.is_ok(), "whitelisted host must be allowed");
let bad = csp_state::check_csp(
Directive::ConnectSrc,
&Url::parse("https://collector-pxu6b0qd2s.px-cloud.net/api/v2/collector").unwrap(),
None,
false,
);
assert!(bad.is_err(), "off-policy connect-src must be blocked");
assert_eq!(bad.unwrap_err(), "connect-src");
csp_state::clear_csp_policy();
}
#[tokio::test]
async fn frame_src_blocks_disallowed_iframe() {
use browser_oxide::csp_collector::collect_csp;
use browser_oxide::html_parser::parse_html;
use browser_oxide::js_runtime::extensions::fetch_ext as csp_state;
use browser_oxide::net::csp::Directive;
use std::sync::Arc;
use url::Url;
let dom = parse_html(
r#"<html><head>
<meta http-equiv="Content-Security-Policy" content="frame-src 'self' https://www.youtube.com">
</head></html>"#,
);
let policy = collect_csp(&[], &dom);
csp_state::set_csp_policy(
Arc::new(policy),
Url::parse("https://example.com/").unwrap(),
true,
);
let yt = csp_state::check_csp(
Directive::FrameSrc,
&Url::parse("https://www.youtube.com/embed/abc").unwrap(),
None,
false,
);
assert!(yt.is_ok());
let bad = csp_state::check_csp(
Directive::FrameSrc,
&Url::parse("https://attacker.example/").unwrap(),
None,
false,
);
assert!(bad.is_err());
assert_eq!(bad.unwrap_err(), "frame-src");
csp_state::clear_csp_policy();
}
#[tokio::test]
async fn frame_src_falls_back_through_child_src_to_default_src() {
use browser_oxide::csp_collector::collect_csp;
use browser_oxide::html_parser::parse_html;
use browser_oxide::js_runtime::extensions::fetch_ext as csp_state;
use browser_oxide::net::csp::Directive;
use std::sync::Arc;
use url::Url;
let dom = parse_html(
r#"<html><head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
</head></html>"#,
);
let policy = collect_csp(&[], &dom);
csp_state::set_csp_policy(
Arc::new(policy),
Url::parse("https://example.com/").unwrap(),
true,
);
let same_origin = csp_state::check_csp(
Directive::FrameSrc,
&Url::parse("https://example.com/iframe.html").unwrap(),
None,
false,
);
assert!(
same_origin.is_ok(),
"default-src 'self' allows same-origin iframe"
);
let cross = csp_state::check_csp(
Directive::FrameSrc,
&Url::parse("https://other.example/iframe.html").unwrap(),
None,
false,
);
assert!(
cross.is_err(),
"default-src 'self' blocks cross-origin iframe via fallback chain"
);
assert_eq!(cross.unwrap_err(), "default-src");
csp_state::clear_csp_policy();
}
#[tokio::test]
async fn bypass_env_var_disables_enforcement() {
use browser_oxide::csp_collector::collect_csp;
use browser_oxide::html_parser::parse_html;
use browser_oxide::js_runtime::extensions::fetch_ext as csp_state;
use browser_oxide::net::csp::Directive;
use std::sync::Arc;
use url::Url;
let dom = parse_html(
r#"<html><head>
<meta http-equiv="Content-Security-Policy" content="connect-src 'none'">
</head></html>"#,
);
let policy = collect_csp(&[], &dom);
csp_state::set_csp_policy(
Arc::new(policy),
Url::parse("https://example.com/").unwrap(),
false,
);
let any = csp_state::check_csp(
Directive::ConnectSrc,
&Url::parse("https://anywhere.test/x").unwrap(),
None,
false,
);
assert!(
any.is_ok(),
"bypass=true must allow even 'none' policy fetches"
);
csp_state::clear_csp_policy();
}