feat(server): TRACKER_ALLOWED_ORIGINS origin allowlist for tracking - #252
rbonestell wants to merge 12 commits into
Conversation
… /tracker.js CORS
…ker.js as ACAO * CORS cannot gate who loads a <script> or POSTs to the worker, so the per-origin ACAO echo on tracker.js enforced nothing. Move real allowlist enforcement to /collect, where data is actually recorded. - tracker.js now returns Access-Control-Allow-Origin: * unconditionally (satisfies SRI/crossorigin, drops Vary:Origin cache fragmentation). - /collect drops (200 gif, no datapoint write) hits whose host (h) or Origin/Referer headers aren't an allowed origin; opt-in via the env var. The analytics referrer param (r) is intentionally not validated. - New app/lib/allowedOrigins.ts: parseAllowedOrigins / extractHost / isHostAllowed (exact + subdomain matching). Hardening from review: - "*" entry => allow-all (empty list) instead of silently blocking all. - extractHost rejects userinfo (https://evil.com@pmux.io/), the opaque "null" origin, and hostless schemes; robust bare-host/port/IPv6 parse. Adds 12 tests (lib + collect) incl. drop-path Last-Modified guard, bare-host e2e, Referer-only, empty-list, and "*" allow-all.
6d8b2e6 to
044290f
Compare
stordahl
left a comment
There was a problem hiding this comment.
Summary
The allowedOrigins implementation is careful (userinfo rejection, null origin, sibling-domain rejection) and well-tested, but requesting changes:
Blocker: CI will fail. $script.ts imports app/tracker/tracker.js?raw, so $script.test.tsx fails with ENOENT unless copytracker ran first. Turbo's test/test-ci tasks don't depend on copytracker, and CI's command (turbo build lint typecheck test-ci --concurrency=1) schedules server#test-ci before the tracker build (reproduced locally). Fix in turbo.json: add dependsOn: ["@counterscale/server#copytracker"] to the server's test and test-ci tasks.
Concerns:
- Fail-open on redeploy — README says set the var via dashboard, but wrangler deploys overwrite dashboard-set vars with
wrangler.json's"". A later CLI redeploys the allowlist away silently. Warn in the README or support it in the CLI. - Silent drops — no log on drop; combined with
hfollowing<link rel="canonical">(cross-domain canonicals get legit traffic dropped), this invites "my dashboard is empty" reports. Log on drop and document the canonical caveat. - Perf regression —
/tracker.jswas edge-served as a static asset; now it invokes the worker on every pageview. Consider keeping the asset path. - Please split the PR — the tracker inlining is orthogonal to the allowlist; separate them for independent review/revert.
Nits: IPv6 entries can't work (normalizeEntry splits on :); add TRACKER_ALLOWED_ORIGINS to .dev.vars.example; revert the package.json reformat that inflates the diff.
…atic asset The ?raw import made server tests depend on copytracker having run (CI ordering failure) and moved every /tracker.js request into the worker. Restore the ASSETS-backed route from main; the inlining can be proposed separately. Also restores package.json formatting and drops the empty TRACKER_ALLOWED_ORIGINS var from wrangler.json so the value can be set as a secret that survives redeploys.
- log a warning on every dropped hit instead of failing silently - normalize allowlist entries via extractHost so bracketed IPv6 works - document setting the var as a secret (dashboard vars are overwritten on redeploy) and the <link rel=canonical> cross-domain caveat - add TRACKER_ALLOWED_ORIGINS to .dev.vars.example; type it optional
|
Addressed in 7366e70 and a05f2b4:
|
Overview
Stacked PR: depends on #251. See the note below.
Adds an opt-in origin allowlist for tracking (
TRACKER_ALLOWED_ORIGINS) so a Counterscale instance only records hits from sites you actually own.Important
This PR is stacked on top of #251 (
fix/custom-dataset-name-bug) and should be reviewed and merged after it.Until #251 lands, the diff against
mainalso includes that PR's changes (CF_DATASET_NAME, the CLI config,packages/cli/*,query.ts,arrow.ts). Those are not part of this PR. The changes that belong here are:packages/server/app/lib/allowedOrigins.ts(+ tests)packages/server/app/analytics/collect.ts(+ tests)packages/server/worker-configuration.d.ts,.dev.vars.exampleREADME.mdTo review only this PR's changes:
git diff <#251 head>..feat/tracker-allowed-origins, or wait for #251 to merge and this diff will shrink to the list above.Changes by Package
@counterscale/cli
No changes in this PR. (CLI/dataset changes visible in the diff belong to #251.)
@counterscale/server
Origin allowlist for tracking (
TRACKER_ALLOWED_ORIGINS) — set a comma-separated list of domains and the instance only records pageviews originating from those domains (and their subdomains):example.commatchesexample.comand all of its subdomains (blog.example.com,app.example.com, …) — you don't list subdomains separately — but not sibling domains likenotexample.com. Entries may be bare hosts, scheme-prefixed (https://example.com), or bracketed IPv6 ([::1]); all normalize to the host./collect(where data is recorded). When the var is set, a hit whose reported host andOrigin/Refererdon't resolve to an allowed domain is dropped — the endpoint still returns the normal 1×1 gif (200) but writes no datapoint, and a warning is logged so dropped traffic is visible in Worker logs. The analytics referrer (the visitor's traffic source) is deliberately not validated.*) means no enforcement; all origins are recorded, preserving today's behavior. The var is intentionally not declared inwrangler.jsonso it can be set as a Worker secret that survives CLI/wranglerredeploys (dashboard plain-text vars get overwritten on deploy).app/lib/allowedOrigins.ts(parse / host-extract / match helpers) plusTRACKER_ALLOWED_ORIGINSwiring in the/collecthandler and type definitions.Tests — new unit + integration coverage for origin parsing/matching (exact, subdomain, sibling-rejection, userinfo,
nullorigin, ports, IPv6 entries,*allow-all) and/collectenforcement (allow/drop, drop-path leavesLast-Modifiedunset, bare-host,Referer-only, opt-in). Full server suite green.@counterscale/tracker
No changes in this PR.
Other Changes
README.md: documents the variable, that it should be set as a secret, the drop logging, and the<link rel="canonical">cross-domain caveat..dev.vars.example: addsTRACKER_ALLOWED_ORIGINS.Additional Notes
Origin,Referer, and the reported host are client-supplied and can be forged by a non-browser client, so it stops accidental/casual cross-site recording and honest browsers, not a determined attacker. Data is partitioned by site ID, which limits the blast radius. Hardening includes rejecting userinfo-bearing values (e.g.https://evil.com@example.com/), the opaquenullorigin, and treating a lone*as allow-all rather than silently blocking everything.TRACKER_ALLOWED_ORIGINSis unset by default; existing deployments are unaffected until it's set.