Summary
content_script.js listens on document for a set of internally-named CustomEvents (ff53054c0e13_crowdQuery, ff53054c0e13_crowdContribute, ff53054c0e13_followAndContribute, ff53054c0e13_ffclipboardSet/Get/Clear) meant to be dispatched only by the extension's own MAIN-world injected script. Because a CustomEvent dispatched on document carries no origin information and the listener performs no check at all, any web page the content script runs on (matches: <all_urls>) can dispatch these events directly from its own page JS and have the content script relay them into privileged chrome.runtime.sendMessage calls to background.js.
Two concrete, live-verified consequences:
- crowdContribute has zero field/domain validation (unlike the sibling followAndContribute handler, which correctly allowlists the target host against fetchDomains). Any webpage can make the background service worker POST completely attacker-controlled body content to https://crowd.fastforward.team/crowd/contribute_v1, silently, on every page load.
- Cross-origin read/write of the shared ffclipboard key/value store (chrome.storage.local) -- any page can Set or Get any key, breaking the per-flow isolation it's meant to provide across bypass pages.
Root cause
document.addEventListener('ff53054c0e13_crowdContribute', (event) => {
let data = event.detail;
chrome.runtime.sendMessage({ type: 'crowdContribute', detail: data });
});
background.js forwards request.detail fields straight into a URLSearchParams POST body with no allowlist:
let params = new URLSearchParams();
if (request.type !== 'followAndContribute') {
for (let key in request.detail) {
params.append(key, request.detail[key]); // no validation, attacker-controlled
}
}
Compare with followAndContribute, which DOES check fetchDomains.includes(dest.hostname) -- the check pattern exists in this file, it's just missing for crowdContribute/ffclipboard*.
Proof of concept (live-verified)
Built the real extension from HEAD, loaded unpacked in headless Chromium via puppeteer-core, hooked fetch in the real background service worker, then from an unrelated page (https://example.org) dispatched the event directly:
document.dispatchEvent(new CustomEvent('ff53054c0e13_crowdContribute', {
detail: { domain: 'attacker-controlled.example', path: 'forged-path',
target: 'https://attacker-controlled.example/payload',
extra_field_attacker_added: 'anything I want, no validation' }
}));
Captured real outbound fetch():
{"url": "https://crowd.fastforward.team/crowd/contribute_v1",
"body": "domain=attacker-controlled.example&path=forged-path&target=https%3A%2F%2Fattacker-controlled.example%2Fpayload&extra_field_attacker_added=anything+I+want%2C+no+validation"}
Second PoC -- ffclipboard leak: page A (example.org) sets key workink, page B (example.com, unrelated origin) reads it back with the forged value intact.
Impact
Crowd-database write abuse: the crowd API is already publicly writable per the project's own docs, so the new capability is using every visiting victim's browser/IP as an anonymizing relay for writes, evading IP-based rate-limiting at scale, silently. ffclipboard: any page can forge/leak values used to correlate multi-hop bypass state.
Suggested fix
Generate a random per-page-load token passed to the injected script (the existing ?ext_base_URL= channel could carry it), require every bridge event's detail to include it. Apply the existing fetchDomains allowlist pattern to crowdContribute too. Scope ffclipboard entries by tab/origin instead of a single global namespace.
This report was produced with AI assistance (Claude, Anthropic): static analysis plus live verification by loading the built extension in a real headless Chromium instance and driving it via CDP/puppeteer-core.
Summary
content_script.js listens on document for a set of internally-named CustomEvents (ff53054c0e13_crowdQuery, ff53054c0e13_crowdContribute, ff53054c0e13_followAndContribute, ff53054c0e13_ffclipboardSet/Get/Clear) meant to be dispatched only by the extension's own MAIN-world injected script. Because a CustomEvent dispatched on document carries no origin information and the listener performs no check at all, any web page the content script runs on (matches: <all_urls>) can dispatch these events directly from its own page JS and have the content script relay them into privileged chrome.runtime.sendMessage calls to background.js.
Two concrete, live-verified consequences:
Root cause
background.js forwards request.detail fields straight into a URLSearchParams POST body with no allowlist:
Compare with followAndContribute, which DOES check fetchDomains.includes(dest.hostname) -- the check pattern exists in this file, it's just missing for crowdContribute/ffclipboard*.
Proof of concept (live-verified)
Built the real extension from HEAD, loaded unpacked in headless Chromium via puppeteer-core, hooked fetch in the real background service worker, then from an unrelated page (https://example.org) dispatched the event directly:
Captured real outbound fetch():
{"url": "https://crowd.fastforward.team/crowd/contribute_v1", "body": "domain=attacker-controlled.example&path=forged-path&target=https%3A%2F%2Fattacker-controlled.example%2Fpayload&extra_field_attacker_added=anything+I+want%2C+no+validation"}Second PoC -- ffclipboard leak: page A (example.org) sets key workink, page B (example.com, unrelated origin) reads it back with the forged value intact.
Impact
Crowd-database write abuse: the crowd API is already publicly writable per the project's own docs, so the new capability is using every visiting victim's browser/IP as an anonymizing relay for writes, evading IP-based rate-limiting at scale, silently. ffclipboard: any page can forge/leak values used to correlate multi-hop bypass state.
Suggested fix
Generate a random per-page-load token passed to the injected script (the existing ?ext_base_URL= channel could carry it), require every bridge event's detail to include it. Apply the existing fetchDomains allowlist pattern to crowdContribute too. Scope ffclipboard entries by tab/origin instead of a single global namespace.
This report was produced with AI assistance (Claude, Anthropic): static analysis plus live verification by loading the built extension in a real headless Chromium instance and driving it via CDP/puppeteer-core.