Skip to main content
A practical field guide to the secure coding habits that stop the vulnerabilities attackers actually exploit in production.

Secure Coding Best Practices Every Developer Needs

KU
Kiril Urbonas
last month 6 min read16 views

A practical field guide to the secure coding habits that stop the vulnerabilities attackers actually exploit in production.

Key takeaways

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.

Never trust input#

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.

Prevent injection#

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.

python.python
# 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.

Encode output for its context#

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.

Authorize every request and every object#

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:

  • Least privilege: grant the minimum access a role needs, nothing more.
  • Deny by default: if no rule explicitly allows an action, refuse it.
  • Server-side checks: never trust a permission decision made in the browser.

For the broader map of what attackers target, the OWASP Top 10 explained is worth keeping open while you review your own routes.

Handle secrets and crypto with care#

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.

Fail closed, log carefully#

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.

Keep dependencies honest#

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.

The habits that matter most#

If you adopt nothing else, adopt these:

  1. Parameterize every query and command. This alone kills the most damaging bug class.
  2. Validate input against an allowlist at the boundary, after canonicalizing.
  3. Check authorization on every object, not just authentication on every session.
  4. Keep secrets out of code and dependencies patched.

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.

The call we'd make#

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.

Explore topics:DevOps
React

Get the DevOps Troubleshooting Cheat Sheet

Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.

Share this post
KU

About Kiril Urbonas

DevOps Engineer

537 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.