A practical field guide to the secure coding habits that stop the vulnerabilities attackers actually exploit in production.
Most breaches do not come from exotic zero-days. They come from ordinary code that trusted the wrong input, concatenated a string it should have parameterized, or skipped an authorization check on one endpoint out of two hundred. Secure coding is not a separate discipline you bolt on at the end. It is a set of habits you carry into every function you write.
This guide skips the compliance checklists and focuses on what actually moves the needle. If you want the wider program context, start with our pillar on application security best practices. Here we stay close to the keyboard.
Every byte that crosses a trust boundary is hostile until proven otherwise. That includes request bodies, query strings, headers, cookies, file uploads, message queue payloads, and responses from third-party APIs you do not control.
Two rules make this concrete:
Validate against an allowlist. Define what is valid and reject everything else. Denylists always miss a case. If a field should be a US state code, check it against the fifty known values, not against a regex that tries to ban bad characters.
Canonicalize before you validate. Decode and normalize input into a single canonical form first, then validate. Otherwise %2e%2e%2f sails past a check looking for ../, and mixed Unicode encodings slip through. Canonicalize, then validate, then use. Never reorder those steps.
Validate at the boundary, as early as possible, so the rest of your code works with data it can trust.
Injection is still the highest-impact class of bug because it turns data into executable instructions. The fix is always the same idea: keep code and data in separate channels.
Use parameterized queries. Never build SQL by concatenating strings.
# Vulnerable: user input becomes part of the query
query = "SELECT * FROM users WHERE email = '" + email + "'"
cursor.execute(query)
# Fixed: input is bound as a parameter, never parsed as SQL
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
The fixed version cannot be tricked by ' OR '1'='1 because the driver never treats the value as query syntax.
Execute commands without a shell. Pass arguments as an array, not a single interpolated string. In most languages that means avoiding shell=True or the shell form of exec, so user input never reaches a command interpreter.
Deserialize safely. Do not deserialize untrusted data into arbitrary objects. Prefer plain data formats like JSON with strict schemas, and disable polymorphic type resolution unless you fully control the input.
The same principle covers LDAP, XML, NoSQL, and template engines. If you find yourself building a command out of strings, stop and look for the parameterized API.
Cross-site scripting happens when data lands in a page without being encoded for where it lands. The safe rule: treat every output destination as a context with its own encoding.
HTML body, HTML attribute, JavaScript, URL, and CSS each need different escaping. A value that is safe inside an HTML text node can still break out inside a <script> block or an unquoted attribute. Use your framework's context-aware auto-escaping and avoid raw sinks like innerHTML, dangerouslySetInnerHTML, or v-html unless you have sanitized the value with a vetted library first.
Authentication proves who someone is. Authorization decides what they may do, and it is where most real-world access bugs live.
Check authorization on every request and on every object the request touches. The classic failure is an endpoint that verifies the user is logged in but never confirms the record they asked for belongs to them. Change id=123 to id=124 and you read someone else's data.
Build on three defaults:
For the broader map of what attackers target, the OWASP Top 10 explained is worth keeping open while you review your own routes.
Never hardcode secrets. No API keys, passwords, or tokens in source, config files committed to git, or client bundles. Load them from a secrets manager or injected environment, and keep them out of logs, stack traces, and error messages returned to users.
Do not roll your own crypto. Use vetted libraries and standard algorithms. Hash passwords with argon2 or bcrypt, never a bare SHA. Use TLS for every connection, internal ones included. If you are writing crypto primitives yourself, you are almost certainly introducing a weakness.
When something goes wrong, default to the secure outcome. An auth check that throws should deny access, not fall through to allow.
Show users a generic error. Detailed stack traces and database messages hand attackers a map of your internals. Log the technical detail server-side instead, but keep secrets, tokens, and personal data out of the logs. Do log security-relevant events: failed logins, authorization denials, and input validation rejections give your team the signal they need to spot an attack in progress.
Your code is a small fraction of what ships. The rest is dependencies, and vulnerable packages are a leading source of incidents. Keep them current and scan them continuously with software composition analysis so a known CVE never rides quietly into production. Our dependency and SCA scanning walkthrough covers how to wire this into a pipeline.
If you adopt nothing else, adopt these:
Everything else is defense in depth around those four. Secure defaults, code review, and SAST are safety nets that catch what a tired developer misses, not substitutes for the habits above.
Pick the two habits your codebase is weakest on and make them non-negotiable in review this week. Parameterized queries and per-object authorization checks give you the largest drop in real risk for the least effort. Wire an SCA scan into CI so dependency drift gets caught automatically, then layer the rest in over time. Secure code is not written in a heroic sprint. It is the accumulation of small, correct decisions repeated on every pull request.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Evergreen posts worth revisiting.