/logs and /metrics: wrong tokens are never counted, so nothing bounds guessing an operator-chosen token #472

Closed
opened 2026-10-01 10:46:57 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the order of the checks in the two token-guarded endpoints and the rate-limit tests.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

Both views check the bearer token first and spend the per-address allowance (POSTULO_ENDPOINT_RATE, 120/h) only once it has matched. A wrong token is answered 401 at once and never counted, so the one limit these shared-secret endpoints have bounds the collector and not somebody trying tokens. Unlike API tokens, which Postulo generates with 32 random bytes, POSTULO_LOGS_TOKEN and POSTULO_METRICS_TOKEN are whatever the operator typed, and nothing checks their length.

  • Where: core/views_logs.py:82-95 (collect: _authorised before throttle.endpoint("logs", request)); core/views_metrics.py:41-56 (scrape, same order); core/throttle.py:151-166 (endpoint); core/checks.py, which validates the rates but not the tokens. tests/security/test_rate_limits.py (test_a_wrong_token_is_still_refused_before_anything_is_counted) records the order as deliberate: "there is no work to protect behind a bad token".
  • What happens: an operator enables /logs with a short, memorable POSTULO_LOGS_TOKEN. Anybody who can reach the instance may present tokens as fast as the server answers; every attempt gets 401 and none is ever refused with 429.
  • Who can trigger it: anyone who can reach the instance, when /logs (or /metrics with a token) is enabled and the operator chose a guessable token. A long random token is out of reach either way, which is why this is low.
  • Why it matters: the log names connections, companies and applications, and the wiki calls an exposed log endpoint "a data leak with a URL". The token is its only guard, and the rate limit that the wiki describes as applying per calling address does not apply to the requests that matter for it.
  • Fix: count refusals under their own key per address, for example throttle.consume(f"endpoint-refused:{name}", address, Rate(10, 3600)) on every 401, and once that is spent answer 429 to the address for the rest of the window whatever it presents. A collector with the right token never spends it, and a stranger's failures cost only the stranger's address. Add a core/checks.py warning when POSTULO_LOGS_TOKEN or POSTULO_METRICS_TOKEN is set and shorter than 24 characters, and say in .env.example and the wiki how to make one (openssl rand -hex 32). Correct the wiki's What /metrics and /logs share to say which requests the limit counts.

Related to #112, which added POSTULO_ENDPOINT_RATE for these endpoints but only for callers already holding the token.

Done when

  • A test in tests/security/test_rate_limits.py sends more wrong tokens than the refusal budget to /logs and gets 429 with Retry-After; the existing test that five wrong tokens are followed by a 200 still passes.
  • A test in tests/test_checks.py sets POSTULO_LOGS_TOKEN="short" and gets the warning.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the order of the checks in the two token-guarded endpoints and the rate-limit tests. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. Both views check the bearer token first and spend the per-address allowance (`POSTULO_ENDPOINT_RATE`, `120/h`) only once it has matched. A wrong token is answered `401` at once and never counted, so the one limit these shared-secret endpoints have bounds the collector and not somebody trying tokens. Unlike API tokens, which Postulo generates with 32 random bytes, `POSTULO_LOGS_TOKEN` and `POSTULO_METRICS_TOKEN` are whatever the operator typed, and nothing checks their length. - **Where:** `core/views_logs.py:82-95` (`collect`: `_authorised` before `throttle.endpoint("logs", request)`); `core/views_metrics.py:41-56` (`scrape`, same order); `core/throttle.py:151-166` (`endpoint`); `core/checks.py`, which validates the rates but not the tokens. `tests/security/test_rate_limits.py` (`test_a_wrong_token_is_still_refused_before_anything_is_counted`) records the order as deliberate: "there is no work to protect behind a bad token". - **What happens:** an operator enables `/logs` with a short, memorable `POSTULO_LOGS_TOKEN`. Anybody who can reach the instance may present tokens as fast as the server answers; every attempt gets `401` and none is ever refused with `429`. - **Who can trigger it:** anyone who can reach the instance, when `/logs` (or `/metrics` with a token) is enabled and the operator chose a guessable token. A long random token is out of reach either way, which is why this is low. - **Why it matters:** the log names connections, companies and applications, and the wiki calls an exposed log endpoint "a data leak with a URL". The token is its only guard, and the rate limit that the wiki describes as applying per calling address does not apply to the requests that matter for it. - **Fix:** count refusals under their own key per address, for example `throttle.consume(f"endpoint-refused:{name}", address, Rate(10, 3600))` on every `401`, and once that is spent answer `429` to the address for the rest of the window whatever it presents. A collector with the right token never spends it, and a stranger's failures cost only the stranger's address. Add a `core/checks.py` warning when `POSTULO_LOGS_TOKEN` or `POSTULO_METRICS_TOKEN` is set and shorter than 24 characters, and say in `.env.example` and the wiki how to make one (`openssl rand -hex 32`). Correct the wiki's *What `/metrics` and `/logs` share* to say which requests the limit counts. Related to #112, which added `POSTULO_ENDPOINT_RATE` for these endpoints but only for callers already holding the token. ## Done when - A test in `tests/security/test_rate_limits.py` sends more wrong tokens than the refusal budget to `/logs` and gets `429` with `Retry-After`; the existing test that five wrong tokens are followed by a `200` still passes. - A test in `tests/test_checks.py` sets `POSTULO_LOGS_TOKEN="short"` and gets the warning.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:46:57 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#472
No description provided.