Skip to content

Hide SQL details in production JSON/NDJSON/SSE/CSV error responses#1308

Merged
lovasoa merged 1 commit into
mainfrom
ophir.lojkine/fix-production-error-leak-machine-formats
Jun 11, 2026
Merged

Hide SQL details in production JSON/NDJSON/SSE/CSV error responses#1308
lovasoa merged 1 commit into
mainfrom
ophir.lojkine/fix-production-error-leak-machine-formats

Conversation

@lovasoa

@lovasoa lovasoa commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

hide SQL details in production error responses for every output format

In production, the HTML renderer already hid DB errors behind a generic message, but the JSON, NDJSON, SSE, and CSV renderers wrote the full error (the .sql path, the exact SQL statement, and the raw DB error) into the response body once streaming had started. Since the format is picked from the Accept header, a visitor could request an HTML page with Accept: application/json and read back its SQL.

Design

One centralized censoring point, no environment checks in the rendering logic:

  • Errors stay full anyhow::Error everywhere. Producers never think about dev/prod.
  • The single place that turns an internal error into a user-facing one is ClientError::new in error.rs. It is also the only caller of DevOrProd::is_prod: in production it strips detail to a generic message, in development it keeps message/backtrace/hint. The full error is always logged server-side.
  • Every renderer (HTML, JSON, NDJSON, SSE, CSV) and the header/pre-body path receives a ClientError and only formats it. None inspect environment. A new output format is safe by construction: it can only render ClientError's already-censored fields.

render.rs now renders components; error.rs owns turning errors into user-facing representations. Moved into error.rs: the ClientError type, the production message, get_backtrace_as_strings, and the error-component data construction.

Verify

The single environment check:

$ grep -rn 'is_prod()' src/render.rs src/webserver/error.rs
src/webserver/error.rs:78:        if environment.is_prod() {

render.rs shrank to mostly renderer dispatch; the error type and policy live in error.rs:

$ git diff --stat origin/main..HEAD -- src/render.rs src/webserver/error.rs
 src/render.rs          | 199 +++++++++++++++++------------
 src/webserver/error.rs | 211 +++++++++++++++++++++++++++++--

Behavior is covered by tests in tests/data_formats/mod.rs: production JSON, CSV, CSV-before-first-row, and an HTML-only page requested as JSON all return the generic message and leak nothing; a unit test in error.rs asserts the production ClientError exposes no sensitive substring at the single boundary.

$ cargo test --test mod data_formats   # 13 passed (incl. 4 prod-leak tests)
$ cargo test --test mod errors         # 9 passed
$ cargo test --lib -- webserver::error render::tests   # 4 passed
$ cargo clippy --all-targets --all-features -- -D warnings   # clean

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf94b4525c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/render.rs Outdated
@lovasoa lovasoa force-pushed the ophir.lojkine/fix-production-error-leak-machine-formats branch 3 times, most recently from 0ab1fe3 to cc50183 Compare June 10, 2026 21:16
…format

In production, error responses must not leak the SQL statement, the source
file path, the raw database error, environment values, or configuration, for
ANY output format. In development the full detail is shown, and the full error
is always logged server-side.

This centralizes the dev-vs-production decision in a single place. An internal
error stays a full `anyhow::Error` everywhere; the only place that turns it into
a user-facing representation is `ClientError::new` in `src/webserver/error.rs`,
which is also the only caller of `DevOrProd::is_prod`. Every renderer (HTML,
JSON, NDJSON, SSE, CSV) and the header/pre-body path obtains a `ClientError`
from that one function and only formats it; none of them inspect the
environment. Leaking is therefore impossible by construction: a renderer cannot
emit what it never receives.

The error type, the production message, `get_backtrace_as_strings`, and the
error-component data construction live in `error.rs`; `render.rs` only renders
components and formats a `ClientError`.
@lovasoa lovasoa force-pushed the ophir.lojkine/fix-production-error-leak-machine-formats branch from cc50183 to 07d2aef Compare June 11, 2026 09:25
@lovasoa lovasoa merged commit 4b487c1 into main Jun 11, 2026
51 checks passed
@lovasoa lovasoa deleted the ophir.lojkine/fix-production-error-leak-machine-formats branch June 11, 2026 09:56
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