Chris Hayes' Journal

“We validate at every boundary because data changes meaning when it crosses contexts. It’s repetitive, but one missed boundary is full compromise.”

When data moves from user input → your code → database → API → another service, each crossing is a boundary. What was “harmless text” in one context becomes “executable SQL” in another. Validate everywhere because you can’t rely on upstream validation—you don’t control what changed the data between there and here.

“We use parameterized queries because strings become code when they hit the interpreter. Concatenation is faster to write, but it’s handing the attacker your execution context.”

SQL injection happens when you build queries with string concatenation: "SELECT * FROM users WHERE id=" + userId. If userId is "1 OR 1=1", you just gave away your entire database. Parameterized queries treat data as data, never as code. The database knows the structure before seeing the values.

“We limit result sets because read access isn’t binary - it’s about volume over time. Convenience wants ‘get all’, but exfiltration happens in bulk.”

A user with read access to customer records might legitimately need to view one record at a time. But if they can query unlimited records, they can dump your entire database in minutes. Pagination and rate limiting turn “read access” from “download everything” into “view what you need.” Data theft requires volume.

“We make authorization explicit in every function because ambient authority spreads silently. Implicit permission is easier, but compromise spreads through trust.”

Don’t check permissions once at the door and trust everything inside. Every function should verify: “Does THIS user have permission to do THIS action on THIS resource?” If an attacker compromises one internal service, explicit authorization stops them from pivoting to everything else. Implicit trust is how one breach becomes total compromise.

“We validate then copy, never transform in place, because the data might change between check and use. Mutation is efficient, but race conditions are invisible.”

Time-of-check-time-of-use (TOCTOU) bugs: You validate data, then use it… but between the check and use, another thread changed it. Always validate, make an immutable copy, then use the copy. If you transform in place, an attacker can race your validation and swap in malicious data after you checked but before you used it.

“We set timeouts on everything because waiting is free for attackers but expensive for us. Infinite patience seems simpler, but resource exhaustion is guaranteed.”

Every connection, database query, API call, lock, and operation needs a timeout. Without timeouts, an attacker can make your system wait forever—slowly consuming all your threads, connections, and memory until nothing works. Timeouts turn “wait forever” into “fail fast and free the resource.”

“We reject unknown fields because today’s ignored data is tomorrow’s exploit vector. Permissive parsing is forgiving, but attackers hide in what you ignore.”

If your API expects {username, password} but ignores extra fields, an attacker sends {username, password, isAdmin: true}. Maybe today you ignore it. Tomorrow someone adds code that reads isAdmin without realizing it’s attacker-controlled. Strict parsing—reject anything unexpected—prevents future you from trusting attacker-supplied data.

“We log before and after, not just errors, because attack success looks like normal operation. Error-only logging is cleaner, but you can’t investigate what you didn’t record.”

Successful attacks don’t throw errors—they look like normal operations. Log authentication attempts (success and failure), authorization checks, data access, privilege escalation attempts. When you discover a breach weeks later, these logs are your only way to understand what happened, when, and what data was taken.

“We separate read and write credentials because compromise is inevitable, impact is controllable. Single credentials are convenient, but blast radius is total.”

When (not if) an attacker compromises your credentials, what can they do? If your app uses one database credential with full permissions, they can read, modify, and delete everything. Separate credentials: read-only for queries, write-only for updates, admin for schema changes. Compromise of read-only credentials = data leak. Compromise of full credentials = total destruction.

“We size-limit everything - inputs, outputs, memory, recursion - because unbounded is attacker-controlled. Trust in reasonable behavior is optimistic, but exploitation targets the unreasonable.”

Unbounded operations are DoS vulnerabilities. No max string length? Send a 10GB string. No recursion limit? Stack overflow. No output size cap? Consume all memory generating results. Every “unlimited” operation is an attacker’s lever to exhaust your resources. Reasonable users stay within bounds; attackers test the extremes.

“We fail closed, not open, because errors are when systems are most vulnerable. Default-allow is user-friendly, but confusion should deny, not permit.”

When something goes wrong (network failure, parse error, unexpected state), what happens? “Fail open” = grant access anyway. “Fail closed” = deny access. If your authorization check throws an exception and you catch it with “allow access,” you just turned every error into a bypass. Errors should always deny, never permit.

“We version and pin dependencies because updates are code changes we didn’t review. Auto-update is convenient, but supply chain compromise is silent.”

Every dependency is code running in your app. Auto-updating means you’re running unreviewed code in production. Supply chain attacks inject malicious code into popular packages. Version pinning means updates are deliberate—you review changelogs, test, then upgrade. Automatic updates = automatic compromise distribution.

“We parse into our types before using because raw formats carry hidden complexity. Pass-through is less code, but parsers are where interpretation happens.”

Don’t pass raw JSON, XML, or user input through your system. Parse it into strongly-typed objects immediately. Parsers detect malformed data, enforce structure, and prevent injection attacks. If you pass raw strings around, eventually someone will interpret them as code. Parse once at the boundary, use typed objects everywhere else.

“We check integer bounds before arithmetic because overflow wraps silently. Assuming math works is natural, but attackers control your calculations through input.”

In many languages, maxInt + 1 wraps to negative numbers. An attacker sends quantity=2147483647 and your calculation price * quantity overflows to negative, resulting in them getting paid to take your products. Always check: Will this addition/multiplication overflow? Is this subtraction going negative? Integer math isn’t safe math.

“We expire everything - tokens, sessions, caches - because infinite lifetime is infinite opportunity. Permanent credentials are simpler, but time amplifies every mistake.”

Session tokens, API keys, password reset links, cached permissions—everything needs an expiration. A stolen session token with no expiration is permanent account access. Even if you detect the breach, you can’t revoke what doesn’t expire. Time limits turn “compromised forever” into “compromised until expiration.” Shorter lifetimes = smaller windows of opportunity.

“We separate code paths for different privilege levels because shared code inherits maximum privilege. Code reuse is efficient, but privilege bleeds through shared functions.”

If admin users and regular users share the same code path, that code needs admin privileges—and bugs in it give regular users admin access. Separate the code: admins use adminDeleteUser(), users use userDeleteOwnAccount(). Each function requests minimum privileges. Shared code with maximum privilege means every bug is a privilege escalation.

“We whitelist, never blacklist, because we can’t imagine all dangerous inputs. Blocking known-bad is intuitive, but attackers are more creative than our blocklist.”

*Blacklisting: “Block

“We treat configuration as code - reviewed, tested, versioned - because config changes behavior as surely as code does. Loose config management is agile, but misconfiguration is exploitation.”

A config change that sets debug=true in production leaks stack traces. A firewall rule typo exposes your database. A permission misconfiguration grants public access. Config changes should go through code review, be version controlled, tested in staging, and deployed with the same care as code changes. Config is code—treat it that way.

“We make errors uninformative to outsiders because details aid reconnaissance. Helpful errors are user-friendly, but stack traces are roadmaps for attackers.”

User gets an error. Helpful: “Database connection failed at line 247 in UserService.java connecting to postgres://prod-db-01:5432.” Attackers now know your stack, database type, hostname, and internal structure. Generic: “An error occurred. Please try again later.” Log details internally, show generic messages externally. Error messages are reconnaissance tools.

“We check authorization after authentication, not assume it, because identity doesn’t imply permission. Trust after login is seamless, but lateral movement is trivial without authorization.”

Authentication = proving who you are. Authorization = proving you’re allowed to do this specific thing. Just because someone logged in doesn’t mean they can access everything. User A shouldn’t access User B’s data. Regular users shouldn’t access admin functions. Check authorization on every action: “Is THIS user allowed to do THIS to THIS resource?” Authentication opens the door; authorization decides what’s in each room.