Summary
/admin/ingest (added in #13) is the push-mode endpoint for the laptop ingester. It is currently protected only by a constant-time X-Ingest-Token header check and a body-size guard. There is no rate limit, so an attacker who guesses or steals the token (or sends unauthenticated requests) can hammer the endpoint freely.
The endpoint is mounted on the live backend even in pull mode, so this is reachable on the public internet today via http://api.fueller.app/admin/ingest and http://52.30.72.127/admin/ingest.
Why it matters
- CPU / memory amplification. A 32 MiB ingest payload (default
ingest.maxBodyBytes) is JSON-parsed and built into Maps before the cache swap. An attacker doesn't need to know the token to make the server burn CPU on parsing; they just need to send a malformed-but-large body.
- 401 brute-force. No rate limit means a token-guessing loop is essentially free.
- Log noise. Every rejected request gets logged; an attacker can fill disks.
Proposed change
Add a token-bucket rate limiter to the /admin/ingest route specifically. Reasonable defaults:
- 10 requests per minute per source IP for unauthenticated/401 responses
- 60 requests per hour per source IP for authenticated 200/4xx responses (legitimate ingesters fire every 30 min, so 60/hr leaves generous headroom)
- Respond with HTTP 429 and a
Retry-After header when exceeded
Ktor has a RateLimit plugin that handles this; can be scoped to a single route so /api/search is unaffected.
Related
Summary
/admin/ingest(added in #13) is the push-mode endpoint for the laptop ingester. It is currently protected only by a constant-timeX-Ingest-Tokenheader check and a body-size guard. There is no rate limit, so an attacker who guesses or steals the token (or sends unauthenticated requests) can hammer the endpoint freely.The endpoint is mounted on the live backend even in pull mode, so this is reachable on the public internet today via
http://api.fueller.app/admin/ingestandhttp://52.30.72.127/admin/ingest.Why it matters
ingest.maxBodyBytes) is JSON-parsed and built into Maps before the cache swap. An attacker doesn't need to know the token to make the server burn CPU on parsing; they just need to send a malformed-but-large body.Proposed change
Add a token-bucket rate limiter to the
/admin/ingestroute specifically. Reasonable defaults:Retry-Afterheader when exceededKtor has a RateLimit plugin that handles this; can be scoped to a single route so
/api/searchis unaffected.Related
/api/search(the public-facing endpoint). They should probably share a single implementation pass, but the threat models are different (/api/searchis high-volume public;/admin/ingestis low-volume privileged), so keeping the issues separate.