Security
Secure Coding Fundamentals
Secure coding is the practice of writing software so that its normal, boring behavior is also its safe behavior - so that a bug is a crash or an error message rather than a way to steal data or take over the system. It applies at every layer: validating input, handling errors without leaking information, keeping trust boundaries clear, and never assuming input is well-formed just because the interface suggests it should be. Most real vulnerabilities are not exotic; they are ordinary bugs - unchecked input, an overly trusting assumption - that happen to have security consequences.
Why it matters
- Most vulnerabilities are ordinary bugs, not exotic attacks
- Unvalidated input, a missing permission check, or an overly trusting assumption about where data came from account for the overwhelming majority of real security issues, not novel cryptographic breaks.
- Fixing a vulnerability after release is far more expensive than avoiding it in the design
- A missing check caught in code review costs a comment; the same gap found after release costs an incident, a patch, and possibly a disclosure to affected users.
- Security bugs often hide behind functionality that appears to work correctly
- Code that handles the happy path perfectly can still be exploitable through inputs or sequences a normal user would never try, which is exactly what makes them easy to miss without deliberately looking.
- Trust boundaries are where nearly everything goes wrong
- The moment data crosses from something a user controls into something the system trusts - a query, a command, a file path - it needs to be treated as unverified until checked.
Never trust input, including your own client-side code
Any validation that runs in a browser or app is a usability convenience, not a security control, because a request can be crafted and sent directly to the server, bypassing the client entirely. The server is the actual trust boundary, so whatever checks matter for safety - not just for a good user experience - have to be repeated there, regardless of what the client already checked.
def create_account(email, age):
if not is_valid_email(email):
raise ValueError("invalid email")
if not (0 < age < 120):
raise ValueError("invalid age")
# only checked, trusted data reaches here
save_account(email, age)Fail closed, not open
When something unexpected happens - an error, a missing case, a check that can't complete - the safe default is to deny the action, not allow it. A permission check that grants access when it hits an unhandled case is a much more dangerous bug than one that denies access when it shouldn't, because the first kind fails silently and the second kind is at least visible as a complaint from a legitimate user.
Least privilege
Give each piece of code, service account or user only the access it actually needs to do its job, not the broadest access that happens to be convenient. That way, a compromise of one part - a bug, a leaked credential - doesn't automatically grant access to everything else the system can touch.
-- least privilege: the app's database user can only do what the app needs
GRANT SELECT, INSERT, UPDATE ON orders TO app_user;
GRANT SELECT ON customers TO app_user;
-- no DROP, no DELETE, no access to other applications' tablesHandling errors without leaking information
A stack trace or a detailed database error shown to a user can reveal file paths, library versions, or the structure of a query - details that make an attacker's next step easier. The safe pattern is to log the full detail somewhere internal, for debugging, and return a short, generic message to whoever made the request.
Mistakes people make here
- Relying on client-side validation as the actual security control
- anything running in the browser or app can be bypassed by sending a request directly, so client-side checks are a usability nicety, not a security boundary; the server has to check again.
- Trusting data because it came from 'inside' the system
- an internal API, a message queue, or a value read back from your own database can still carry attacker-controlled content if it was written by a less-trusted part of the system earlier; where data is validated matters more than where it currently lives.
- Showing detailed error messages or stack traces to users
- a stack trace can reveal file paths, library versions, or query structure that makes an attacker's job easier; detailed errors belong in server-side logs, not in the response.
- Rolling a custom authentication or cryptographic scheme instead of using a well-reviewed library
- these are deceptively easy to get subtly wrong in ways that look fine in testing and only fail under deliberate misuse; established libraries have had far more scrutiny than a one-off implementation will get.
- Treating security as a final review step instead of part of design
- a trust boundary or permission model bolted on after the architecture is set is far more likely to have gaps than one considered from the start.
Strengths and trade-offs
Where it is strong
- Most of it is disciplined habit rather than specialized knowledge: validate input, check permissions, fail closed, avoid leaking detail - the same handful of practices prevent most real issues.
- Catching an issue during code review or design is far cheaper than catching it after release.
- The core ideas - trust boundaries, least privilege, defense in depth - apply across every language and framework, so the skill doesn't need relearning each time the stack changes.
The trade-offs
- Defensive checks add code and a small amount of runtime cost everywhere data crosses a trust boundary; this is close to always worth it, but it's not free.
- It's impossible to prove a system has no vulnerabilities, only to reduce the likelihood and blast radius of the ones that exist - security work has no clean finish line.
- Being appropriately defensive without becoming so restrictive the software is unusable is a real balance, and reasonable engineers can disagree on where a given line sits.
Who needs this
Every developer who writes code that runs with any level of trust or handles any input from outside the program - which is nearly all application code. A dedicated security team goes much deeper, but the baseline habits here are everyone's job, not just theirs.
Questions about secure coding fundamentals
- Isn't security the security team's job, not mine?
- A dedicated security team can review, test and set standards, but they don't write most of the code that ships; the habits that prevent the majority of real vulnerabilities - validating input, not trusting the client, failing closed - have to be part of how every developer writes code day to day.
- Why not just add security at the end, right before release?
- Some decisions - access control models, how trust boundaries are drawn, what data a service is even allowed to see - are architectural and expensive to change late. A review before release catches some issues, but it can't retrofit a design that never considered them.
- What's the single highest-leverage habit here?
- Treating all input as untrusted until it's validated - not just form fields, but URL parameters, headers, file contents, and data from other internal services. A large share of real vulnerabilities trace back to some input being trusted a step earlier than it should have been.
- Should I build my own validation and sanitization functions?
- For basic checks, like an age range or a required field, plain code is fine. For anything security-sensitive - password hashing, encoding output for a specific context, parsing untrusted file formats - prefer a well-maintained library over a custom implementation, since these have edge cases that are easy to miss and have already been found and fixed in mature libraries.