Download-beacon rate limiting (WAF applier + wizard) (security F5) - #192
Merged
Conversation
sparkyfen
commented
Jul 17, 2026
| if (!host) return { status: 'error', detail: 'no domain given' }; | ||
|
|
||
| // 1. Resolve the zone id. | ||
| const zoneRes = await api(cfToken, `/zones?name=${encodeURIComponent(host)}`); |
Contributor
Author
There was a problem hiding this comment.
Security · CONFIRMED — subdomain-hosted forks can never attach the WAF rule (F5 protection silently not applied)
applyDownloadRateLimit resolves the zone with an exact GET /zones?name=<host> match. Any fork whose site host is a subdomain of its Cloudflare zone gets an empty result and the misleading error token has no access to zone <host>: add Zone → WAF: Edit, even when the token is correctly scoped.
- e.g. site
sub.example.comin zoneexample.com, or the staging hostbeta.sona.fastfrom feat(staging): staging environment for beta.sona.fast (#166) #171. - The sibling setup preflight already walks
zoneNameCandidates()for the identical lookup (see harden(setup): CLI follow-ups from the #51 gate rounds (13+1 items) #179), so it finds the zone while this step fails on the same host.
Result: the operator chases a nonexistent permissions problem and the download beacon (POST /api/metrics/download) stays unprotected against the write-flood this PR exists to close.
Fix: reuse zoneNameCandidates(host) here instead of the exact-name query.
…ain hosts applyDownloadRateLimit looked up the zone by exact host name, so a fork served from a subdomain of its zone got a false 'token has no WAF access' error and the download-beacon rate limit was never applied. Walk zone-name candidates (host, then leading labels stripped to the registrable domain), mirroring the setup preflight.
Cloudflare counts rate-limit rules per data center outside Enterprise, and the Rulesets API rejects a rule whose characteristics omit cf.colo.id (HTTP 400, code 20155). The download-beacon rule sent only ip.src, so the apply failed on every zone. Count per-IP-per-colo instead.
…lity The Free plan is "not entitled to use a mitigation timeout different from 10", so a 60s block was rejected (HTTP 400). 10 is valid on every plan, keeping the rule portable across forks; a blocked IP is denied for 10s then must re-exceed the threshold.
sparkyfen
force-pushed
the
fix/download-waf-ratelimit
branch
from
July 19, 2026 02:47
b091ab9 to
6927b6b
Compare
sparkyfen
added a commit
that referenced
this pull request
Jul 27, 2026
#192 touched the same three places in setup.ts. Both features are additive and independent, so every conflict keeps both sides: the two lib imports, the two provisioning calls, the two status variables, and the two Next-steps blocks. Nesting matters and is preserved — the WAF apply stays inside the resolved-zone block, Turnstile stays outside it (account-scoped, needs no zone) but inside the token check. TOKEN_RECIPE now lists both new scopes, with Turnstile in the Account group rather than appended after the Zone ones. Dropped the note telling the reader not to add Zone · WAF · Edit here — that was a merge-coordination marker for exactly this merge, and it is now stale.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the mechanism to protect the download-metrics beacon with a Cloudflare rate-limit rule (audit finding F5, Option 1 — WAF at the edge). This PR only adds code; it does not touch any Cloudflare account. The rule is applied only when the applier is explicitly run with a token, or during a future fork's setup.
scripts/waf-lib.ts—applyDownloadRateLimit(token, domain): idempotently creates a rate-limit rule (POST /api/metrics/download, 20 req / 10s per IP, block 60s) via the Rulesetshttp_ratelimitphase. Matched on a stableref, so re-runs find-and-skip. Cannot delete or overwrite an operator's other WAF rules (PATCHes only our rule by id; POST appends; the whole-set PUT fires only when the zone has zero rate-limit rules; rate-limiting is a separate phase from custom firewall rules). The token is never logged and never leaves the Authorization header.scripts/setup.ts): future forks with a custom domain provision the rule automatically; a token lacking the scope produces a non-fatal warning (setup never crashes), and*.pages.dev-only forks skip it.npm run apply-download-ratelimit -- <domain>): for existing forks; reads the token fromCLOUDFLARE_API_TOKEN(env only, never argv).Reviewed by a security gate (PASS): confirmed no collateral WAF-rule damage and no token leakage. 13 unit tests (mocked CF, no network) + full suite (995) pass.
Note: applying the rule to any live account is a separate, consent-gated step (operator adds the token scope; run against your own zone first).