37deb9d907b8

fix Firefox Docker sender-context relay and verify real authorization boundaries

time
2026-09-04 14:12 UTC · 1 month ago
machine
koh2
agent
claude-sonnet-5
session
close Firefox security findings
parent
469a2ef18467
2 files scripts/test-firefox-sender-context.mjs+21 −8tests/fixtures/firefox-sender-probe.js+10 −4
scripts/test-firefox-sender-context.mjs+21 −8
@@ -201,6 +201,9 @@201201       method: "POST",202202       body: JSON.stringify({ url: "https://example.com/" }),203203     });204+    // Firefox may finish navigation before document_start content scripts have205+    // registered their relay listener; wait for the extension boundary to settle.206+    await new Promise((resolveDelay) => setTimeout(resolveDelay, 2000));204207     const result = await webdriverRequest(port, `/session/${sessionId}/execute/async`, {205208       method: "POST",206209       body: JSON.stringify({@@ -210,17 +213,23 @@210213           const send = (message) => new Promise((resolve) => {211214             const id = crypto.randomUUID();212215             const timer = setTimeout(() => {213-              window.removeEventListener("message", onMessage);216+              window.removeEventListener("message", onResponse);214217               resolve({ ok: false, error: "probe_timeout" });215218             }, 5000);216-            function onMessage(event) {217-              if (event.source !== window || event.data?.source !== source || event.data.id !== id) return;219+            function onResponse(event) {220+              let response;221+              try {222+                response = JSON.parse(event.data);223+              } catch {224+                return;225+              }226+              if (response?.source !== source + ":response" || response.id !== id) return;218227               clearTimeout(timer);219-              window.removeEventListener("message", onMessage);220-              resolve(event.data.response);228+              window.removeEventListener("message", onResponse);229+              resolve(response.response);221230             }222-            window.addEventListener("message", onMessage);223-            window.postMessage({ source, id, message }, window.location.origin);231+            window.addEventListener("message", onResponse);232+            window.postMessage(JSON.stringify({ source, id, message }), "*");224233           });225234           Promise.all([226235             send({ type: "GET_SETTINGS" }),@@ -228,6 +237,7 @@228237             send({ type: "GET_RECOVERY_WORDS" }),229238           ]).then(([allowed, entries, recovery]) => done({230239             pageOrigin: location.origin,240+            probeReady: document.documentElement.dataset.peachFirefoxSenderProbe ?? null,231241             allowed,232242             entries,233243             recovery,@@ -240,12 +250,15 @@240250     if (result.pageOrigin !== "https://example.com") {241251       throw new Error(`Unexpected page origin: ${result.pageOrigin}`);242252     }253+    if (result.probeReady !== "ready") {254+      throw new Error(`Firefox content-script probe did not execute: ${JSON.stringify(result)}`);255+    }243256     if (result.allowed?.error === "invalid_sender") {244257       throw new Error("Content-script-allowed GET_SETTINGS was rejected");245258     }246259     for (const [name, response] of [["GET_ENTRIES", result.entries], ["GET_RECOVERY_WORDS", result.recovery]]) {247260       if (response?.error !== "invalid_sender") {248-        throw new Error(`${name} was not rejected from the real Firefox content-script sender: ${JSON.stringify(response)}`);261+        throw new Error(`${name} was not rejected from the real Firefox content-script sender: ${JSON.stringify({ result, response })}`);249262       }250263     }251264     console.log("Firefox sender-context test passed: real content-script GET_ENTRIES and GET_RECOVERY_WORDS were rejected, GET_SETTINGS remained allowed.");

view file

tests/fixtures/firefox-sender-probe.js+10 −4
@@ -1,11 +1,17 @@11 (() => {22   const SOURCE = "peach-firefox-sender-probe";3+  document.documentElement.dataset.peachFirefoxSenderProbe = "ready"; // Proves Firefox injected this content script before the page probe runs.34   const ALLOWED_TYPES = new Set(["GET_ENTRIES", "GET_RECOVERY_WORDS", "GET_SETTINGS"]);45 56   window.addEventListener("message", async (event) => {6-    if (event.source !== window || event.data?.source !== SOURCE) return;7-    const { id, message } = event.data;8-    if (typeof id !== "string" || !message || !ALLOWED_TYPES.has(message.type)) return;7+    let request;8+    try {9+      request = JSON.parse(event.data);10+    } catch {11+      return;12+    }13+    const { id, message } = request ?? {};14+    if (request?.source !== SOURCE || typeof id !== "string" || !message || !ALLOWED_TYPES.has(message.type)) return;915 1016     let response;1117     try {@@ -13,6 +19,6 @@1319     } catch (error) {1420       response = { ok: false, error: String(error) };1521     }16-    window.postMessage({ source: SOURCE, id, response }, window.location.origin);22+    window.postMessage(JSON.stringify({ source: `${SOURCE}:response`, id, response }), "*");1723   });1824 })();

view file