From 0c4740a86abe650383c935a42f3293e2b3301a0e Mon Sep 17 00:00:00 2001 From: alectimison-maker Date: Fri, 31 Jul 2026 04:44:21 +0800 Subject: [PATCH] fix(js): execute dynamic data URL scripts Decode JavaScript data URLs inside the serialized classic-script queue so each body runs before its load event advances a nested chain. Fixes #520. --- .../tests/dynamic_script_onload_fires.rs | 79 +++++++++++++++++++ crates/obscura-js/js/bootstrap.js | 46 +++++++++-- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs b/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs index d80a92e..3967cdb 100644 --- a/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs +++ b/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs @@ -117,3 +117,82 @@ async fn dynamic_external_scripts_execute_and_fire_load() { "dynamic scripts must execute and fire load before navigation settles" ); } + +#[tokio::test(flavor = "current_thread")] +async fn dynamic_data_scripts_execute_before_chained_load_handlers() { + let mut ctx = CdpContext::new(); + let page_id = ctx.create_page(); + let session_id = "session-1"; + ctx.sessions.insert(session_id.to_string(), page_id); + + cdp( + &mut ctx, + 1, + "Page.navigate", + json!({ + "url": "data:text/html,", + "waitUntil": "load" + }), + session_id, + ) + .await; + + cdp( + &mut ctx, + 2, + "Runtime.evaluate", + json!({ + "expression": r#"(function () { + var state = { + aExec: false, + aLoad: false, + bExec: false, + bLoad: false, + cExec: false, + cLoad: false + }; + window.__dataScriptState = state; + + var a = document.createElement('script'); + a.src = 'data:text/javascript,window.__dataScriptState.aExec=true'; + a.onload = function () { + state.aLoad = true; + var b = document.createElement('script'); + b.src = 'data:application/javascript,' + encodeURIComponent('window.__dataScriptState.bExec=true'); + b.onload = function () { + state.bLoad = true; + var c = document.createElement('script'); + c.src = 'data:text/javascript;base64,' + btoa('window.__dataScriptState.cExec=true'); + c.onload = function () { state.cLoad = true; }; + document.head.appendChild(c); + }; + document.head.appendChild(b); + }; + document.head.appendChild(a); + return 'kicked'; + })()"#, + "returnByValue": true, + }), + session_id, + ) + .await; + + ctx.pages[0].settle(500).await; + + let result = cdp( + &mut ctx, + 3, + "Runtime.evaluate", + json!({ + "expression": "JSON.stringify(window.__dataScriptState)", + "returnByValue": true, + }), + session_id, + ) + .await; + assert_eq!( + result["result"]["value"], + r#"{"aExec":true,"aLoad":true,"bExec":true,"bLoad":true,"cExec":true,"cLoad":true}"#, + "data URL script bodies must execute before each chained load handler" + ); +} diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 3ef371a..cc17415 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -18,7 +18,7 @@ '__obscura_hw', '__obscura_mem', '__documentReadyState__', '__currentUrl', // internal helpers (var-declared throughout the file) - '__processDynScriptQueue', '_markNative', '_fpRand', '_fpNoise', + '__processDynScriptQueue', '_decodeDataScriptUrl', '_markNative', '_fpRand', '_fpNoise', '_fpCache', '_getFp', '_fp', '_splitAsciiWhitespace', '_getElementsByClassName', '_docEncoding', '_docIsUtf8', '_isSpecialScheme', '_applyDocQueryEncoding', '_anchorBase', @@ -163,6 +163,37 @@ let _fpSeed = 0; // when SPAs dynamically insert multiple