Skip to content

fix(logging): mask one-time codes in logged request URLs - #674

Merged
Valtora merged 1 commit into
mainfrom
fix/access-log-secrets
Oct 23, 2026
Merged

Valtora merged 1 commit into
mainfrom
fix/access-log-secrets

Conversation

@Valtora

@Valtora Valtora commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Pull Request

Description

uvicorn's access log writes each request line with its query string, and the bundled nginx writes the request line and the Referer in full. Some request URLs carry a one-time code: the calendar sign-in callbacks (code, state, and Microsoft's session_state), an invitation link (/register?invite=<code>, followed by /api/v1/invitations/validate/<code>), and Microsoft Graph's webhook validation (validationToken). Both logs are in the Settings log download, so they can end up in a bug report.

What is masked. The value of each of these query parameters becomes <redacted>, matched by name without regard to case: code, state, session_state, access_token, refresh_token, id_token, token, ticket, client_secret, api_key, validationToken, invite, invite_code. The code in /api/v1/invitations/validate/<code> is masked too. The path, every other parameter and the rest of the line stay as they were, and an empty value stays empty.

Where.

  • backend/utils/url_redaction.py holds the list and redact_url_secrets. SensitiveDataFilter now applies it to every string it already sanitises, so the API's and workers' own log lines mask such a URL in a message or an exception's text, and also the calendar sync cursors (syncToken, pageToken, $deltatoken, $skiptoken).
  • install_log_redaction() attaches that filter to uvicorn.access and to uvicorn.error, where uvicorn writes WebSocket handshake lines. backend/main.py calls it at import, which covers the container's uvicorn backend.main:app, python -m backend.main and --reload. setup_logging calls it as well.
  • nginx/access-log.conf, included from the http block of nginx.conf, defines nojoin_combined: nginx's combined format with the request line and the Referer passed through a chain of regex maps.

httpx. Its per-request HTTP Request: lines at INFO are now held at WARNING, as urllib3 and requests already are. No outgoing request carries a credential in its query string: the Gemini SDK sends its key in a header, and every token exchange is a POST body. The calendar lines did carry the provider's sync cursor, and the calendar id in the path, which for a Google calendar is the account's address. The calendar code already logs its own failures with the status and the provider's error, addresses masked.

Query strings checked.

  • Calendar callbacks, GET /api/v1/calendar/oauth/{google,microsoft}/callback: code, state, session_state. Masked by the API and nginx.
  • Microsoft webhook validation, POST /api/v1/calendar/webhooks/microsoft?validationToken=: masked by both.
  • Model listing, GET /api/v1/llm/models?api_key=: the API accepts it, the web app does not send it. Masked by both.
  • Invitation page /register?invite=, in nginx's request line and in the Referer of the requests that page makes, and GET /api/v1/invitations/validate/<code>: masked by both.
  • MCP consent page /oauth/authorize?...&state=, in nginx's request line and Referer: state masked. code_challenge is a hash meant to be public and stays. /api/v1/oauth/authorize/info carries no state.
  • Nothing to mask: /api/v1/system/logs/live authenticates with the session cookie and carries only container, capture and live transcript requests put no token in a URL, backup downloads use the cookie, the CLI sign-in takes its pasted code in a POST body, Google's webhook authenticates by header, and the MCP token endpoint takes a form body.

Decision record

  • Mask values, keep names and the rest of the line. Logging the path alone would be simpler, but it drops parameters that help read a request, such as container= or a calendar result's reason.
  • A logger filter set from Python, not a --log-config file. It covers every way the API starts, the dev runs included, with no Dockerfile or compose change.
  • One filter class. SensitiveDataFilter already guards the root handlers, so a name added to the list reaches the application logs and uvicorn's together.
  • A fixed list of names, matched whole. write_token, a note's idempotency key, and names that merely contain a listed one (decode, states) are left alone. No route uses state or code for anything else.
  • httpx held at WARNING rather than its lines masked. Masking the cursor would still leave the address in the path.
  • nginx: four maps for the request line, two for the Referer. A map substitutes once, so each one masks the last parameter not yet masked. Four cover the three the Microsoft callback carries; a fifth listed parameter in one request would stay. The test replays the maps in Python and checks their name list against the API's.
  • Left as is: nginx's error log quotes the request line of a request that failed upstream, and its format cannot be set. That needs the API to be down during a sign-in.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behaviour)
  • Documentation update

Checks run

backend/tests/test_access_log_redaction.py sends a Microsoft callback request through a real uvicorn server and checks its access line: code, state and session_state masked, scope and the path kept, and the line otherwise in uvicorn's own format. Other tests cover a request with nothing to mask (line unchanged), the WebSocket handshake line, httpx no longer logging each request, redact_url_secrets cases, and the nginx maps replayed against a callback request, an invitation path and Referer, and a page with nothing to mask. The masking tests fail on main.

Migration impact

  • No database migration.
  • Adds an Alembic migration: one checked-in head, no committed revision deleted or renamed, upgrade and downgrade behaviour described above, and python3 scripts/check_migrations.py run against a throwaway Postgres.

Documentation impact

  • No documentation change required.
  • Updated the guide that owns each changed behaviour, setup, deployment or support topic (.github/CONTRIBUTING.md, "Documentation Ownership"): an operator rule in docs/SECURITY.md, and item 9 of the forwarding checklist in docs/DEPLOYMENT.md.

Security impact

  • No security-sensitive change.
  • Touches auth, tokens, encryption, capture ownership or exposure; docs/SECURITY.md boundaries preserved and updated where behaviour changed.

Manual verification

nginx -t on the new config, then the config in nginx:alpine with a closed upstream: both callbacks, the webhook validation, the invitation page and its validate call with the page as Referer, the consent page as Referer, the model listing, and a settings page with nothing to mask. Each line came out as the tests expect, and a missing Referer still logs as -.

Existing installs pick up the nginx change with git pull and docker compose restart nginx. The API and worker changes arrive with their images.

uvicorn's access log, and nginx's in the bundled proxy, wrote each request
URL whole, so a calendar sign-in callback left its code, state and
session_state in a log Settings offers for download. Both now mask the values
of those parameters, invitation codes and the usual credential names, and keep
the path and every other parameter as they were.

httpx's per-request INFO lines move to WARNING. They carried calendar ids,
which for a Google account is its address, and the provider's sync cursor.
@gitguardian

gitguardian Bot commented Oct 23, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
38915348 Triggered Generic High Entropy Secret 2eafa21 backend/tests/test_access_log_redaction.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@Valtora
Valtora merged commit 5c16323 into main Oct 23, 2026
21 checks passed
@Valtora
Valtora deleted the fix/access-log-secrets branch October 23, 2026 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant