Trust Boundaries & Input Validation
A trust boundary is the line where data crosses from somewhere you do not control into somewhere you do. At its root, every serious security failure is a trust boundary that was not enforced: input treated as safe that was not. Know exactly where your boundaries are, validate hard at each one, and never trust the same data twice.
Inside a boundary you can reason about data. Outside it, anything is possible, including data sent on purpose to attack you. The discipline is to find every boundary (the public API, a webhook, a queue message, a file upload, even another internal service) and to validate, canonicalise, and constrain data as it crosses. Then everything downstream can treat it as trusted, because it has been made trustworthy.
The subtle trap is to assume data is safe because of where it came from: our own database, an internal service, a 'trusted' partner. It is not. Unvalidated input you stored yesterday is still unvalidated input today. The Finperiti webhook finding is the clear example: a boundary (the internet) was treated as trusted because the data looked internal.
Validate at the boundary
- DoFind every trust boundary explicitly — public endpoints, webhooks, queue and event consumers, uploads, and calls from other services — and validate at each one.
- AlwaysValidate against an allow-list: define what is acceptable (type, range, length, format, set) and reject everything else, rather than blocking known-bad.
- DoCanonicalise and normalise input before you validate it, so encoded or alternate forms cannot slip past the check.
- DoVerify authenticity at the boundary for inbound integrations — signatures, tokens — before acting on the payload at all.
- ConsiderConverting validated input into typed, constrained domain objects at the boundary, so an 'unvalidated string' cannot travel deeper into the system.
- NeverAct on an inbound webhook or third-party callback without first verifying its signature or authenticity.
[HttpPost("/webhooks/kyc")]
public IActionResult OnKyc(KycResult body) {
customer.Status = body.Status; // acted on, unverified
...
}
Anyone on the internet can POST a forged 'approved' result. The boundary (untrusted internet to trusted decision) was never enforced. This is exactly the Finperiti unsigned-webhook failure.
if (!signature.Verify(rawBody, header)) return Unauthorized();
var result = Parse(rawBody); // validate shape and allowed values
if (!result.IsKnownStatus) return BadRequest();
Apply(result);
Authenticity is proven, the payload is validated against allowed values, and only then does it cross into trusted logic.
Don't re-trust, don't under-trust
- DoUse safe APIs for every downstream sink — parameterised queries for SQL, safe encoders for output, safe path and command APIs — so a missed validation is not automatically exploitable.
- DoRe-validate at each boundary the data crosses. Data that was safe in one context (display) may be dangerous in another (a query, a shell, a file path).
- DoTreat data read back from your own store, or received from internal services, as input. Validate it for the new context too.
- ConsiderValidating outputs and inter-service messages, not just user input. A compromised or buggy neighbour is a hostile source.
- Do notTrust a value because of its origin (internal, our DB, a partner). Origin is not validation.
- NeverBuild SQL, shell commands, file paths, or markup by joining unvalidated input as strings. Use parameterised queries and safe APIs.
Self-review checklist
- AskWhere exactly does untrusted data enter here, and is it validated at that line?
- AskAm I validating against an allow-list of what is permitted, or just filtering known-bad?
- AskFor inbound integrations, is authenticity verified before any action is taken?
- AskAm I trusting any value just because of where it came from?