Zum Inhalt springen
Docs

Befehlspalette

Zu einer Seite oder Ansicht springen oder ein Todo per Name finden.

Änderungen

Residual multi-source risk on anonymous endpoints

The repository's architectural decision, rationale and consequences.

Markdown

Status: accepted · Date: 2026-11-07 · Related: ADR-0017, ADR-0019, ADR-0020, ADR-0021

Enforcement: packages/api/src/features/municipality/municipality.procedures.test.ts (a 4th new opt-in from one source in a day is refused, line 529; updates and the retry of a lost first answer are not new, line 541; the 21st call from one address in an hour is refused, a withdrawal never, line 567); packages/api/src/platform/rpc/rate-limit.test.ts (the daily source key rotates by UTC day and separates addresses and secrets).

Context

The municipality opt-in is public and anonymous (ADR-0020). The only handle on a caller is the address the edge vouches for, context.ip (packages/api/src/platform/rpc/context.ts:122; cf-connecting-ip on Workers, packages/api/src/platform/rpc/rate-limit.ts:57-63). A district is published only from 20 homes up (MIN_HOMES_PER_DISTRICT = 20, packages/preparedness/src/aggregates.ts:9, applied at packages/api/src/features/municipality/municipality.service.ts:251). The attack is therefore fabricated homes: 19 of them put one real home into a published district.

The review (docs/varu/evidence/review-recheck-2.md, standards#3) marked the fix PARTLY: the per-source cap blocks a single-source burst, but seven sources can still fabricate 19 homes.

Decision

  1. Keep the caps as coded; no code change.
    • 20 calls per address per hour for municipality.opt-in (ANONYMOUS_LIMITS, packages/api/src/platform/rpc/rate-limit.ts:65), applied by anonymousRateLimit (packages/api/src/features/municipality/municipality.procedures.ts:65).
    • 3 new opt-ins per source per day: NEW_OPT_INS_PER_SOURCE_PER_DAY = 3 (rate-limit.ts:80), window NEW_OPT_IN_WINDOW_SEC (rate-limit.ts:81), enforced in optIn (municipality.service.ts:140-161). The source key contains the UTC date, so the count restarts at midnight UTC.
  2. Source key: dailySourceKey(context.ip, BETTER_AUTH_SECRET, now) is the SHA-256 of the server secret, the UTC date and the address (rate-limit.ts:90-93, called at municipality.procedures.ts:70-72). The counter stores the hash, never the address. Rotating BETTER_AUTH_SECRET changes every key and so restarts the counters.
  3. Only new opt-ins count. A call with a token (an update) returns before the source check (municipality.service.ts:123-139). A retry of a lost first answer is matched by its next (municipality.service.ts:141-147). Withdrawals (optOut, municipality.procedures.ts:80-85) are not rate-limited.
  4. The server is the only enforcer. The app does not mirror the caps; it refreshes its own count at most once a day (COUNT_ME_REFRESH_MS, apps/mobile/src/features/settings/settings.count-me.ts:25).
  5. Accepted for v1. No device attestation (App Attest / Play Integrity). Revisit together with the open question in ADR-0017: whether a municipality needs more than these caps.

Consequences

  • A source adds at most 3 new homes a day. 19 fabricated homes need 7 sources in one day, or 7 days from one source (3 × 6 = 18 < 19).
  • Many addresses still get past it, since each address is its own source (for example IPv6 ranges, botnets or rotating mobile carriers).
  • Updates meet only the 20-calls-per-address-per-hour cap.
  • The hourly key keeps the raw address (anon:<limit>:<ip>, rate-limit.ts:105) in the rate_limit table until packages/api/src/platform/jobs/auth-cleanup.ts deletes rows idle for 24 h (AUTH_CLEANUP_GRACE_MS, line 14). The daily source key holds no address. ADR-0017 says the table never stores an address; that holds only for the source key.
  • A request without an address is limited neither hourly nor daily (rate-limit.ts:102, municipality.procedures.ts:70). The code states that production on Workers always has one (rate-limit.ts:95-99).
  • Family sync and learned barcodes have per-address caps only, with no per-source daily cap: family.create 5 and family.push 120 per address per hour (rate-limit.ts:67-68; packages/api/src/features/family/family.procedures.ts:39, :74), and barcodes.learn 30 per address per hour (rate-limit.ts:66; packages/api/src/features/barcodes/barcodes.procedures.ts:45). Many addresses are bounded only by these numbers. The exposure there is volume (families, storage, votes), not disclosure: family data is ciphertext (ADR-0021) and learned barcodes carry no household (ADR-0019).
Auf GitHub bearbeitenDie Dokumentation ist auf Englisch.