| name | defensive-programming |
| description | Use when writing or reviewing code that crosses a trust boundary (user input, external APIs, file/network/DB I/O) — guards against invalid input, injection, unsafe failure modes, and missing validation at the edges without over-defending internal code. |
Defensive Programming at Trust Boundaries
The discipline of defending code where it actually matters — system
boundaries — without smothering internal code in redundant checks. Over-
defending is its own bug: it hides real failures, balloons code for no
safety gain, and makes intent harder to read.
Steps
-
Identify the actual trust boundaries first. Defense belongs at the
edges where untrusted data enters: user input, external API responses,
file/network/DB reads, deserialized payloads, environment/config. Code
that only ever receives data already validated by an internal caller does
not need to re-validate — trust your own contracts.
-
At each boundary, check for the OWASP-class basics relevant to it:
- Injection (SQL/command/template/log) — use parameterized queries,
never string-concatenate untrusted input into commands or queries
- Deserialization — validate shape/type before trusting structure;
never
eval/deserialize untrusted data into executable form
- Auth/access — verify identity and authorization at the boundary,
not just one or the other
- Resource exhaustion — bound input size, recursion depth, request
rates where the boundary is externally reachable
- Secrets — never log, echo, or include credentials/tokens in error
messages or traces that might reach untrusted eyes
-
Fail safely and legibly:
- Reject invalid input with a clear error at the boundary — don't let
malformed data silently propagate inward and fail confusingly later
- Don't swallow exceptions into empty catch blocks — that converts a loud,
diagnosable failure into a silent, undebuggable one
- Error messages to untrusted callers should not leak internals (stack
traces, query structure, file paths); detailed diagnostics belong in logs
-
Don't defend against the impossible. Validating internal function
arguments that the type system or an internal contract already guarantees
is noise — it adds code, obscures the real boundary checks, and signals
"I don't trust my own code," which erodes the value of having contracts
at all. If you find yourself validating the same thing at every layer,
that's a sign the check belongs at the boundary, once.
Output format when reviewing
## Trust boundaries identified
- <boundary> — <what crosses it>
## Findings
- **Missing/weak validation:** `file:line` — <what's unguarded, realistic exploit/failure>
- **Over-defended (noise):** `file:line` — <redundant check that obscures intent>
## Verdict
<are the actual boundaries adequately covered? what's the highest-priority gap?>
Notes
- This pairs with
impact-review — a missing
boundary check is a blast-radius concern (it's how bad data gets in) and
an over-defended internal layer is a maintenance/readability cost.
- When proposing a fix for a boundary gap, ground it in the specific threat
it closes — "this prevents X from reaching Y unsanitized" — not a generic
"added validation for safety."