Logs: httpx records every outbound URL at INFO, so staff read other people's capture, webhook and connection addresses #548

Closed
opened 2026-10-01 10:47:33 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, by reading the logging configuration and watching what an outbound request writes.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at core/server_views.py:476-505 (LogsView) and plugins/http.py:139-145 (_build); config/settings/base.py:798-808 and the webhook lines are where they were. The same request repeated on today's code wrote the same record, with the httpx logger still at level 20.

httpx logs every request it makes at INFO, as HTTP Request: <METHOD> <full URL> "<status>". The root logger runs at INFO and LOGGING quietens only fontTools and weasyprint, so each of those lines reaches the console and the JSON file log, which is what Server settings → Logs and the /logs endpoint serve. The URLs are other people's data: the postings they capture, their portfolio links, the paths on their DAV and Paperless servers, and their webhook receiver addresses. Some receivers treat the address itself as the only secret (a Home Assistant /api/webhook/<id>, an n8n /webhook/<uuid>). The threat model promises that administrators see accounts, never anyone's applications or documents; this crosses that line and writes credentials into the log (CWE-532).

  • Where: config/settings/base.py:798-808 ("loggers" names only fontTools and weasyprint; root level from POSTULO_LOG_LEVEL, default INFO); the file log read by LogsView (core/server_views.py:474-503, logs.read, logs.loggers) and served by core/views_logs.py:collect; requests made through plugins/http.py:143 (_build), for example plugins/webhook/__init__.py:241-242, called from the scheduler at notifications/webhooks.py:131.
  • What happens: checked on the snapshot: a POST through postulo.plugins.http.client() to https://ha.example/api/webhook/SECRETID?x=1 produced the record httpx INFO HTTP Request: POST https://ha.example/api/webhook/SECRETID?x=1 "HTTP/1.1 200 OK", with the effective level of the httpx logger at 20. Under the default policy the host is replaced by the pinned address, but the path and query are logged in full; with private destinations allowed, the host name is logged as well. core/logs.py has no redacting filter; install_record_factory adds only a request id.
  • Who can trigger it: nobody has to do anything unusual. Every staff account can open Server settings → Logs and filter by the httpx logger; a log collector holding POSTULO_LOGS_TOKEN receives the same lines.
  • Why it matters: a staff member learns which postings each person is chasing, and can fire any person's automation whose receiver authenticates by its address alone.
  • Fix: add "httpx": {"level": "WARNING"} and "httpcore": {"level": "WARNING"} to LOGGING["loggers"], as was done for WeasyPrint. If a request trail is wanted, log it from a response hook in plugins/http._build with the scheme, host and status only, never the path or query.

Done when

  • A test in tests/test_logs.py makes a request through plugins.http.client() against a mock transport and asserts that no record at INFO or below contains the request's path.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, by reading the logging configuration and watching what an outbound request writes. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `core/server_views.py:476-505` (`LogsView`) and `plugins/http.py:139-145` (`_build`); `config/settings/base.py:798-808` and the webhook lines are where they were. The same request repeated on today's code wrote the same record, with the `httpx` logger still at level 20. `httpx` logs every request it makes at INFO, as `HTTP Request: <METHOD> <full URL> "<status>"`. The root logger runs at INFO and `LOGGING` quietens only `fontTools` and `weasyprint`, so each of those lines reaches the console and the JSON file log, which is what Server settings → Logs and the `/logs` endpoint serve. The URLs are other people's data: the postings they capture, their portfolio links, the paths on their DAV and Paperless servers, and their webhook receiver addresses. Some receivers treat the address itself as the only secret (a Home Assistant `/api/webhook/<id>`, an n8n `/webhook/<uuid>`). The threat model promises that administrators see accounts, never anyone's applications or documents; this crosses that line and writes credentials into the log (CWE-532). - **Where:** `config/settings/base.py:798-808` (`"loggers"` names only `fontTools` and `weasyprint`; root level from `POSTULO_LOG_LEVEL`, default `INFO`); the file log read by `LogsView` (`core/server_views.py:474-503`, `logs.read`, `logs.loggers`) and served by `core/views_logs.py:collect`; requests made through `plugins/http.py:143` (`_build`), for example `plugins/webhook/__init__.py:241-242`, called from the scheduler at `notifications/webhooks.py:131`. - **What happens:** checked on the snapshot: a POST through `postulo.plugins.http.client()` to `https://ha.example/api/webhook/SECRETID?x=1` produced the record `httpx INFO HTTP Request: POST https://ha.example/api/webhook/SECRETID?x=1 "HTTP/1.1 200 OK"`, with the effective level of the `httpx` logger at 20. Under the default policy the host is replaced by the pinned address, but the path and query are logged in full; with private destinations allowed, the host name is logged as well. `core/logs.py` has no redacting filter; `install_record_factory` adds only a request id. - **Who can trigger it:** nobody has to do anything unusual. Every staff account can open Server settings → Logs and filter by the `httpx` logger; a log collector holding `POSTULO_LOGS_TOKEN` receives the same lines. - **Why it matters:** a staff member learns which postings each person is chasing, and can fire any person's automation whose receiver authenticates by its address alone. - **Fix:** add `"httpx": {"level": "WARNING"}` and `"httpcore": {"level": "WARNING"}` to `LOGGING["loggers"]`, as was done for WeasyPrint. If a request trail is wanted, log it from a response hook in `plugins/http._build` with the scheme, host and status only, never the path or query. ## Done when - A test in `tests/test_logs.py` makes a request through `plugins.http.client()` against a mock transport and asserts that no record at INFO or below contains the request's path.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:33 +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#548
No description provided.