fix(cors): scope moz-extension wildcard to aw-watcher-web endpoints - #166
Conversation
Firefox gives every extension a unique random origin, so aw-server uses `moz-extension://*` to allow aw-watcher-web. That wildcard also permits any other installed Firefox extension to reach the full API — export, import, queries, settings — with no host permission and no install-time browser prompt naming ActivityWatch. This was reported and confirmed in 2026-07. Add `aw_server/extension_cors.py` which registers a `before_request` hook that restricts wildcard-matched extension origins to the three endpoints aw-watcher-web actually needs: GET /api/0/info — version/hostname detection POST /api/0/buckets/aw-watcher-web-<id> — ensure its bucket POST /api/0/buckets/aw-watcher-web-<id>/heartbeat — heartbeats All other paths return 403 before the handler executes. Requests without an Origin header (native watchers, curl) and non-moz-extension origins are unaffected. Origins the user explicitly configured via `cors_origins` or `cors_regex` are also exempted (deliberate opt-ins). Path matching uses split segments, not the raw path string, to avoid percent-encoding bypasses — the bug class from aw-server-rust#588 and #636. Accepted limitation (same as the Rust side, #637): the bucket prefix is a coarse scope — a malicious extension can still send heartbeats to an existing `aw-watcher-web-*` bucket. Full origin-to-bucket ownership binding requires a future pairing/code-exchange protocol. Mirrors ActivityWatch/aw-server-rust#637 for the Python server. Co-Authored-By: Bob <TimeToBuildBob@users.noreply.github.com>
Greptile SummaryThe PR scopes wildcard Firefox-extension access to the API endpoints required by aw-watcher-web.
Confidence Score: 5/5The PR appears safe to merge. The previously reported configured-origin matching mismatch is resolved, and no blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming request] --> B{moz-extension origin?}
B -- No --> F[Continue normally]
B -- Yes --> C{Explicitly configured origin?}
C -- Yes --> F
C -- No --> D{Allowed watcher-web method and path?}
D -- Yes --> F
D -- No --> E[Return HTTP 403]
Reviews (2): Last reviewed commit: "fix(cors): match configured origin regex..." | Re-trigger Greptile |
|
@greptileai review |
|
@TimeToBuildBob What happened to lint CI? passed on master 5d ago. |
|
The repository was using the floating I pinned the lint action to the last known-good SHA in |
|
CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click. This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted. |
Summary
Firefox assigns every extension a unique random origin, so
aw-serverusesmoz-extension://*to allowaw-watcher-webwithout knowing the ID in advance. That wildcard also permits any other installed Firefox extension to reach the full API (export, import, query, settings) with no host permission and no install-time browser prompt naming ActivityWatch.Reported privately by pal0x, who also found the api_key bypass fixed in ActivityWatch/aw-server-rust#636. See ActivityWatch/aw-server-rust#637 (the Rust sibling, now merged) for full background and the verified exploit trace; pal0x specifically asked that the Python server get parity, since the README still lists it as the default implementation.
Changes
Adds
aw_server/extension_cors.py— abefore_requesthook that restricts wildcard-matched extension origins to the three endpointsaw-watcher-webactually uses:/api/0/info/api/0/buckets/aw-watcher-web-<id>/api/0/buckets/aw-watcher-web-<id>/heartbeatEverything else (export, import, query, settings, event reads, bucket deletion, non-watcher buckets) returns 403 before the handler executes.
Design notes
Originheader (native watchers, curl) are unaffectedmoz-extension://origins are unaffected (handled by flask-cors as before)cors_originsare exempt (explicit opt-ins by the server owner)Accepted limitation
Same as the Rust side: the bucket prefix (
aw-watcher-web-) is a coarse scope, not an origin-to-bucket ownership boundary. A malicious extension can still send heartbeats to an existingaw-watcher-web-*bucket. Eliminating that requires a future pairing/code-exchange protocol (the long-term Step 2 in the security write-up).Tests
32 new tests covering:
_is_allowed()and_is_allowed_path()with parametrized cases (allowed paths, blocked paths, percent-encoded bypass attempt)All 41 tests pass (9 existing + 32 new).
Related