diff --git a/src/js/background.js b/src/js/background.js index 2e07228f..7ca05f30 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -116,7 +116,28 @@ brws.runtime.onStartup.addListener(() => { brws.storage.local.set({ version: brws.runtime.getManifest().version }); }); -brws.runtime.onMessage.addListener((request, _, sendResponse) => { +// Fields the crowd-contribute/query bridge accepts, matching the documented +// signature of crowdQuery/crowdContribute in helpers/dom.js. Anything else in +// request.detail is dropped rather than forwarded -- the content-script bridge +// has no way to verify the message actually came from the extension's own +// injected script rather than the hosting page itself (see security advisory +// GHSA-vw3r-qv4c-vw79 / GHSA-5vfw-qqg5-35wj-style report), so background.js is +// the last point that can still enforce a shape on what gets sent out. +const CROWD_FIELDS = { + crowdQuery: ['domain', 'path'], + crowdContribute: ['domain', 'path', 'target'], + followAndContribute: ['domain', 'path', 'target'], +}; + +brws.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === 'getTabId') { + // sender.tab.id is populated by the browser itself from the real + // connection the message arrived on -- a page script cannot forge it, + // unlike anything carried in a CustomEvent's `detail`. + sendResponse(sender.tab ? sender.tab.id : null); + return false; + } + (async () => { let options = await getOptions(); if (options.optionCrowdBypass === false) { @@ -127,14 +148,24 @@ brws.runtime.onMessage.addListener((request, _, sendResponse) => { ? (url = 'https://crowd.fastforward.team/crowd/query_v1') : (url = 'https://crowd.fastforward.team/crowd/contribute_v1'); + const allowedFields = CROWD_FIELDS[request.type]; + if (!allowedFields) { + return; + } + let params = new URLSearchParams(); if (request.type !== 'followAndContribute') { - for (let key in request.detail) { - params.append(key, request.detail[key]); + for (let key of allowedFields) { + if (Object.prototype.hasOwnProperty.call(request.detail, key)) { + params.append(key, request.detail[key]); + } } } else { - for (let key in request.detail) { + for (let key of allowedFields) { + if (!Object.prototype.hasOwnProperty.call(request.detail, key)) { + continue; + } if (key === 'target') { let dest = new URL(request.detail[key]); if (!fetchDomains.includes(dest.hostname)) { diff --git a/src/js/content_script.js b/src/js/content_script.js index f42b49a0..6968f307 100644 --- a/src/js/content_script.js +++ b/src/js/content_script.js @@ -1,4 +1,16 @@ const brws = typeof browser !== 'undefined' ? browser : chrome; + +// ffclipboard keys are namespaced by the real tab id, as reported by the +// browser itself via background.js's sender.tab.id -- a page script cannot +// forge this the way it can forge a CustomEvent's `detail`. This stops an +// unrelated tab/page from reading or poisoning another tab's in-flight +// ffclipboard state. +const ffClipboardTabId = brws.runtime.sendMessage({ type: 'getTabId' }); +async function ffClipboardKey(key) { + const tabId = await ffClipboardTabId; + return `${tabId}:${key}`; +} + async function getOptions() { return new Promise((resolve) => { brws.storage.local.get('options').then((result) => { @@ -71,19 +83,21 @@ document.addEventListener('ff53054c0e13_followAndContribute', (event) => { }); }); -function onFFClipboardSet(event) { +async function onFFClipboardSet(event) { const { key, value } = event.detail; + const scopedKey = await ffClipboardKey(key); chrome.storage.local.get('ffclipboard', (result) => { const ffclipboard = result.ffclipboard || {}; - ffclipboard[key] = value; + ffclipboard[scopedKey] = value; chrome.storage.local.set({ ffclipboard }); }); } -function onFFClipboardGet(event) { +async function onFFClipboardGet(event) { const { key } = event.detail; + const scopedKey = await ffClipboardKey(key); chrome.storage.local.get('ffclipboard', (result) => { - const value = result.ffclipboard ? result.ffclipboard[key] : undefined; + const value = result.ffclipboard ? result.ffclipboard[scopedKey] : undefined; const responseEvent = new CustomEvent('ff53054c0e13_ffclipboardResponse', { detail: { key, value }, }); @@ -91,11 +105,12 @@ function onFFClipboardGet(event) { }); } -function onFFClipboardClear(event) { +async function onFFClipboardClear(event) { const { key } = event.detail; + const scopedKey = await ffClipboardKey(key); chrome.storage.local.get('ffclipboard', (result) => { if (result.ffclipboard) { - delete result.ffclipboard[key]; + delete result.ffclipboard[scopedKey]; chrome.storage.local.set({ ffclipboard: result.ffclipboard }); } });