From 402de261aacc1923723141895b8ccc76cb2f4f41 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:28:42 +0000 Subject: [PATCH] js: fix Blob-URL Worker race and update location synchronously on navigation URL.createObjectURL stored blob content asynchronously (blob.text().then) but the Worker constructor read __blobStore synchronously, so new Worker(blobURL) in the same tick raced the store, fell through to fetch(blobURL), and failed with net::ERR_FAILED. This broke Blob-URL workers including the AWS WAF challenge proof-of-work worker. Store synchronously from Blob._bytes, which the obscura Blob materializes in its constructor. location.reload was a no-op, so challenges that reload after setting a token cookie never re-fetched. It now navigates like assign/replace. location.href/assign/replace/reload cleared __virtualUrl immediately, so a script that set location.href and re-read it saw the old URL until the async navigation completed; real browsers update location.href synchronously. Preview the resolved target in __virtualUrl and clear it in __obscura_init once the navigation completes, so immediate reads match and redirects still win. --- crates/obscura-js/js/bootstrap.js | 34 +++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index a4e2d3a..445e8e0 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -2499,7 +2499,7 @@ function __currentUrl() { } globalThis.location = { get href() { return __currentUrl(); }, - set href(url) { globalThis.__virtualUrl = null; Deno.core.ops.op_navigate(_resolveUrl(url), 'GET', ''); }, + set href(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); }, get origin() { try { return new URL(this.href).origin; } catch { return ""; } }, get protocol() { try { return new URL(this.href).protocol; } catch { return ""; } }, get host() { try { return new URL(this.href).host; } catch { return ""; } }, @@ -2509,14 +2509,14 @@ globalThis.location = { get hash() { try { return new URL(this.href).hash; } catch { return ""; } }, get port() { try { return new URL(this.href).port; } catch { return ""; } }, toString() { return this.href; }, - assign(url) { globalThis.__virtualUrl = null; Deno.core.ops.op_navigate(_resolveUrl(url), 'GET', ''); }, - reload() {}, - replace(url) { globalThis.__virtualUrl = null; Deno.core.ops.op_navigate(_resolveUrl(url), 'GET', ''); }, + assign(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); }, + reload() { var r = _resolveUrl(this.href); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); }, + replace(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); }, }; const _locationObj = globalThis.location; Object.defineProperty(globalThis, 'location', { get() { return _locationObj; }, - set(url) { Deno.core.ops.op_navigate(_resolveUrl(String(url)), 'GET', ''); }, + set(url) { var r = _resolveUrl(String(url)); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); }, configurable: false, enumerable: true, }); @@ -6218,11 +6218,25 @@ globalThis.Worker = class Worker { }; globalThis.__blobStore = globalThis.__blobStore || {}; -const _origCreateObjectURL = URL.createObjectURL; URL.createObjectURL = function(blob) { - if (blob && typeof blob.text === 'function') { + if (blob) { const id = 'blob:obscura/' + Math.random().toString(36).substring(2); - blob.text().then(text => { globalThis.__blobStore[id] = text; }); + // Store synchronously so a Worker built from the blob URL in the same + // tick sees its source. Blob-URL Worker construction is synchronous in + // real browsers; the previous async blob.text().then() store raced the + // Worker constructor, so new Worker(blobURL) fell through to fetch() and + // failed (net::ERR_FAILED), which broke AWS WAF's proof-of-work worker. + // The obscura Blob materializes _bytes in its constructor; fall back to + // the async text() store only for foreign Blob shims without _bytes. + if (blob._bytes) { + let text = ''; + try { text = new TextDecoder().decode(blob._bytes); } catch (e) {} + globalThis.__blobStore[id] = text; + } else if (typeof blob.text === 'function') { + blob.text().then(text => { globalThis.__blobStore[id] = text; }); + } else { + globalThis.__blobStore[id] = ''; + } return id; } return 'blob:obscura/fallback'; @@ -6733,6 +6747,10 @@ if (typeof ShadowRoot !== 'undefined' && !ShadowRoot.prototype.elementFromPoint) globalThis.__obscura_init = function() { _fpSeed = Date.now() ^ (Math.random() * 0xFFFFFFFF >>> 0); _fpCache = null; + // A real navigation just completed (this runs after set_url), so drop any + // URL a location setter previewed synchronously and let document_url drive + // location.href again, including any redirect target. + globalThis.__virtualUrl = null; _installWasmStreamingFallback(); globalThis.document = new Document(+_dom("document_node_id"));