Sitemap

Hardened by Design: A Developer’s Field Guide to Writing Secure Code

5 min readApr 10, 2026

--

Security vulnerabilities are rarely the result of exotic exploits — they’re almost always the result of small, avoidable mistakes made during development. Most breaches don’t start with a zero-day. They start with a forgotten input check, a hardcoded credential, or a query built from a string concat. This guide focuses on eliminating exactly those mistakes — before an attacker gets the chance to find them.

Press enter or click to view image in full size
A Developer’s Guide to Writing Secure Code

1. Never trust input — validate everything at the boundary

Every piece of data that enters your application from an external source — user forms, API calls, file uploads, environment variables — is hostile until proven otherwise. Input validation should happen at the first entry point, not deep inside your business logic.

  • Allowlisting over blocklisting- Define what IS valid, not what isn’t.
  • Type enforcement- Reject wrong types at parse time.
  • Length limits- Bound all string and array lengths.
  • Encoding checks- Normalize Unicode before comparison.

OWASP A03 (Injection) remains in the top 3 every year — almost entirely caused by unsanitized input flowing into SQL engines, shell interpreters, or template renderers.

2. Use parameterized queries — no exceptions

SQL injection is decades old and still devastatingly common. The fix is equally old and equally simple: never concatenate user input into a query string. Use parameterized statements or prepared statements with bound parameters every time.

Vulnerable

query = “SELECT * FROM users WHERE email = ‘“ + email + “‘“
# Input: admin’ — → full authentication bypass

Safe

cursor.execute(“SELECT * FROM users WHERE email = %s”, (email,))
# The driver handles escaping — the query structure is fixed

The same logic applies to NoSQL, LDAP, XPath, OS commands, and XML parsers. Any context where your data is interpreted as code requires structural separation.

3. Store secrets out of your codebase

API keys, database credentials, and private certificates have no place in source code. They get committed, pushed to public repos, and exposed in build logs. Use environment variables for local development and a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) in production.

Never do this

DB_PASSWORD = “s3cr3tp@ssw0rd”
STRIPE_KEY = “sk_live_4xT8…”

Do this instead

DB_PASSWORD = os.environ[“DB_PASSWORD”]
# Or pull at runtime from your secrets manager SDK

Run git log — all — full-history — “**/*.env” on any repo you inherit. You will be surprised what you find — even in private repos.

4. Enforce least privilege everywhere

Your database user should not have DROP TABLE privileges. Your microservice should not have write access to buckets it only reads from. Your API token should expire. The principle of least privilege limits blast radius — when (not if) a component is compromised, the attacker gets the minimum footprint possible.

This applies to: database roles, IAM policies, service account scopes, OAuth scopes, filesystem permissions, and even which environment variables a process can read.

Get Rajitha Haridas’s stories in your inbox

Join Medium for free to get updates from this writer.

5. Hash passwords correctly — bcrypt, Argon2, or scrypt only

MD5 and SHA-256 are not password hashing algorithms. They are fast — which is exactly the wrong property. A modern GPU can compute billions of SHA-256 hashes per second. Use purpose-built, slow, memory-hard functions.

# Python — using bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

# Or Argon2id via argon2-cffi (preferred for new systems)
ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
hash = ph.hash(password)

Never roll your own crypto. Pick a well-audited library and let it handle the salt, iteration count, and algorithm versioning.

6. Output encoding prevents XSS

Cross-site scripting occurs when untrusted data is rendered as HTML without encoding. The fix is context-aware output encoding: HTML-encode data going into HTML, JavaScript-encode data going into JS strings, URL-encode data going into query parameters.

Unsafe

return f”<p>Hello, {username}</p>”
# username = “<script>document.location=’…’</script>”

Safe

return f”<p>Hello, {html.escape(username)}</p>”
# Use your framework’s built-in auto-escaping (Jinja2, React JSX, etc.)

Use your framework’s templating system with auto-escaping enabled by default. Never disable it. Use | safe or dangerouslySetInnerHTML only when you have explicitly sanitized the content with a library like DOMPurify.

7. Implement security headers and HTTPS everywhere

HTTP security headers are one of the cheapest, highest-value controls you can implement. They instruct the browser on how to handle your content and significantly reduce the surface area for XSS, clickjacking, and mixed-content attacks.

Content-Security-Policy: default-src ‘self’; script-src ‘self’
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin

Run your app through securityheaders.com and Mozilla Observatory. An A+ score takes less than an afternoon to achieve on most stacks.

8. Log security events — and only security events

Insufficient logging is OWASP A09. You cannot respond to an incident you cannot detect.

Log authentication attempts (success and failure), authorization failures, input validation rejections, admin actions, and unusual access patterns.

What you should never log: passwords (even failed ones), session tokens, credit card numbers, or PII beyond what’s strictly necessary. Logging sensitive data turns your log pipeline into an additional attack surface.

Structure your logs as JSON with a consistent schema. Ship them to a centralized SIEM (Splunk, Elastic, Datadog). An alert that fires 5 minutes after an anomaly beats a forensic investigation 5 weeks later.

9. Keep dependencies audited and updated

The average application has hundreds of transitive dependencies. Any one of them could carry a known CVE. Integrate SCA (Software Composition Analysis) into your CI pipeline so vulnerabilities are caught before they ship.

# In CI — fail the build on high-severity findings
npm audit — audit-level=high
pip-audit — require-hashes -r requirements.txt
trivy fs — exit-code 1 — severity HIGH,CRITICAL .

Pin your dependency versions and use a lock file. Use Dependabot or Renovate to automate PR creation for updates — keeping the maintenance overhead low enough that developers actually do it.

10. Threat model before you code

The most effective time to fix a security vulnerability is before it’s written. Spend 30 minutes on a feature’s threat model: who are the actors, what are the assets, what can go wrong (spoofing, tampering, repudiation, information disclosure, DoS, privilege escalation — STRIDE), and what controls are you putting in place?

Further reading: OWASP Top 10 (2021) · NIST Secure Software Development Framework (SSDF) · CWE/SANS Top 25 Most Dangerous Software Weaknesses · Google’s Building Secure and Reliable Systems (free PDF) · OWASP ASVS v4.0 for structured requirements.

Security is not a feature you add at the end of a sprint — it’s a property of every design decision made along the way. The practices above aren’t advanced; they’re the baseline. Build them into your team’s definition of “done.”

--

--