Error-Handling Security
Rules (for AI agents)
ALWAYS
- Catch exceptions at the boundary — HTTP handler, RPC method, message consumer —
and decide there what crosses to the client and what stays server-side. Record
the failure with a correlation ID under
logging-security's policy; do not bypass
its redaction rules to attach "full context", which is how request bodies, cookies
and tokens end up in logs.
- Put only client-actionable information in an external error: a stable error code, a
short human-readable message, and the correlation ID. Anything the caller cannot
act on belongs in the log entry that shares that ID.
- Keep responses consistent within an error class, and make security-equivalent
outcomes externally indistinguishable. Different classes may and should differ —
400 VALIDATION_FAILED, 401, 403, 409, 429 are not a finding. What must not
differ is a pair of outcomes whose distinction reveals protected state: whether an
account exists, whether a record the caller may not see is present.
auth-security owns making the login paths cost the same; this rule owns what
they return.
- Disable developer error pages and detailed exception rendering in every
non-development environment, and route unhandled errors through the framework's
production error handler. This is a wiring decision, not the same thing as an
environment variable reading
production — the setting and its name differ per
framework, and references/framework-error-pages.md has them.
- Render every external error through one helper, so the sanitization rules live in a
single place instead of being re-derived at each handler.
- Consult
api-security for the status code and response envelope, logging-security
for the internal record, and auth-security for making two authentication failures
cost the same. This skill owns one decision — what crosses the boundary.
NEVER
- Serialize exception or debug internals into an external response: stack traces, SQL
fragments and constraint names, filesystem paths, internal hostnames, dependency or
framework versions.
- Silently swallow an unexpected exception — a bare
except: pass, catch {},
rescue nil. Let it reach the nearest designated error boundary, which logs it and
converts it to a sanitized response. A narrowly-typed, commented suppression of an
expected condition is fine; the finding is the broad or unexplained one.
- Withhold client-actionable validation detail in order to make the API harder to
explore, or smuggle server state into it. Which field failed and why is the
public contract — that is what RFC 9457 problem details and the GraphQL
errors array are for — and it belongs in the response. Record existence,
authorization state, and SQL or schema internals are not the contract and do
not.
KNOWN FALSE POSITIVES
- Developer diagnostics in an explicitly local development environment that no
untrusted caller can reach. The safety comes from the deployment, not from the
hostname:
localhost and *.local are not security boundaries, and a debug
console that executes code is dangerous wherever it is reachable.
- An authenticated, authorized diagnostic or admin endpoint returning more detail
than a public one. It still may not return secrets, credentials or PII —
authenticating the caller does not make that data less sensitive, and a health
check should answer with status, not with internals.
Context (for humans)
CWE-209 is small text with large consequences: it is how an attacker moves from "this
service exists" to "this service runs Spring 5.2 on Tomcat 9 with a PostgreSQL table
called users and a column called email_normalized". Every extra detail lowers the
cost of the next attack.
The line this skill draws is client-actionable versus internal, not
detailed-versus-generic. Those get confused, and the confusion is expensive in both
directions: an API that flattens every validation failure into an opaque 400 is
hostile to its own callers without being meaningfully safer, while one that returns a
database constraint name has handed over its schema. email must be a valid address
is the public contract. users.email_normalized violates users_email_key is not.
Ownership is deliberately narrow. What a log entry may contain belongs to
logging-security; making two authentication paths take the same amount of work
belongs to auth-security; HTTP status semantics and the response envelope belong to
api-security. This skill owns one decision — what crosses the boundary — and the
handler that enforces it.
References
1---2name: error-handling-security3description: Decide what an error tells the client and what stays server-side: no stack traces, SQL, or paths in responses, no silent suppression, and no distinction between outcomes that would reveal protected state. Use when generating HTTP, GraphQL, or RPC error handlers, exception, panic, or rescue blocks, or configuring production error pages.4---56# Error-Handling Security78## Rules (for AI agents)910### ALWAYS11- Catch exceptions at the boundary — HTTP handler, RPC method, message consumer —12 and decide *there* what crosses to the client and what stays server-side. Record13 the failure with a correlation ID under `logging-security`'s policy; do not bypass14 its redaction rules to attach "full context", which is how request bodies, cookies15 and tokens end up in logs.16- Put only client-actionable information in an external error: a stable error code, a17 short human-readable message, and the correlation ID. Anything the caller cannot18 act on belongs in the log entry that shares that ID.19- Keep responses **consistent within an error class**, and make security-equivalent20 outcomes externally indistinguishable. Different classes may and should differ —21 `400 VALIDATION_FAILED`, `401`, `403`, `409`, `429` are not a finding. What must not22 differ is a pair of outcomes whose distinction reveals protected state: whether an23 account exists, whether a record the caller may not see is present.24 `auth-security` owns making the login paths cost the same; this rule owns what25 they return.26- Disable developer error pages and detailed exception rendering in every27 non-development environment, and route unhandled errors through the framework's28 production error handler. This is a wiring decision, not the same thing as an29 environment variable reading `production` — the setting and its name differ per30 framework, and `references/framework-error-pages.md` has them.31- Render every external error through one helper, so the sanitization rules live in a32 single place instead of being re-derived at each handler.33- Consult `api-security` for the status code and response envelope, `logging-security`34 for the internal record, and `auth-security` for making two authentication failures35 cost the same. This skill owns one decision — what crosses the boundary.3637### NEVER38- Serialize exception or debug internals into an external response: stack traces, SQL39 fragments and constraint names, filesystem paths, internal hostnames, dependency or40 framework versions.41- Silently swallow an unexpected exception — a bare `except: pass`, `catch {}`,42 `rescue nil`. Let it reach the nearest designated error boundary, which logs it and43 converts it to a sanitized response. A narrowly-typed, commented suppression of an44 expected condition is fine; the finding is the broad or unexplained one.45- Withhold client-actionable validation detail in order to make the API harder to46 explore, or smuggle server state into it. Which field failed and why is the47 **public contract** — that is what RFC 9457 problem details and the GraphQL48 `errors` array are for — and it belongs in the response. Record existence,49 authorization state, and SQL or schema internals are **not** the contract and do50 not.5152### KNOWN FALSE POSITIVES53- Developer diagnostics in an explicitly local development environment that no54 untrusted caller can reach. The safety comes from the deployment, not from the55 hostname: `localhost` and `*.local` are not security boundaries, and a debug56 console that executes code is dangerous wherever it is reachable.57- An authenticated, authorized diagnostic or admin endpoint returning more detail58 than a public one. It still may not return secrets, credentials or PII —59 authenticating the caller does not make that data less sensitive, and a health60 check should answer with status, not with internals.6162## Context (for humans)6364CWE-209 is small text with large consequences: it is how an attacker moves from "this65service exists" to "this service runs Spring 5.2 on Tomcat 9 with a PostgreSQL table66called `users` and a column called `email_normalized`". Every extra detail lowers the67cost of the next attack.6869The line this skill draws is **client-actionable versus internal**, not70detailed-versus-generic. Those get confused, and the confusion is expensive in both71directions: an API that flattens every validation failure into an opaque `400` is72hostile to its own callers without being meaningfully safer, while one that returns a73database constraint name has handed over its schema. `email must be a valid address`74is the public contract. `users.email_normalized violates users_email_key` is not.7576Ownership is deliberately narrow. What a log entry may contain belongs to77`logging-security`; making two authentication paths take the same amount of work78belongs to `auth-security`; HTTP status semantics and the response envelope belong to79`api-security`. This skill owns one decision — what crosses the boundary — and the80handler that enforces it.8182## References8384- `references/verifying-findings.md` — confirm or refute a finding, then lock it85- `references/framework-error-pages.md` — turning off developer error pages per86 framework, and why the environment variable is not the switch87- `rules/error_response_template.json`88- [OWASP Error Handling Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html).89- [RFC 9457 — Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html).90- [CWE-209](https://cwe.mitre.org/data/definitions/209.html) · [CWE-755](https://cwe.mitre.org/data/definitions/755.html).