Security is not optional. Every day, attackers scan for vulnerabilities. Every line of code you write is either secure or a potential attack vector. Security is not something you add at the end; it's a foundational practice from day one.
Cross-Site Scripting (XSS)
XSS allows attackers to inject malicious scripts into your application. If your application displays user input without sanitization, an attacker can execute arbitrary JavaScript in visitors' browsers.
Stored XSS
Malicious script is stored in your database:
// Vulnerable code
const comment = req.body.comment;
db.comments.insert({ text: comment }); // Stored in database
// User visits page, attacker's script runs
app.get("/comments", (req, res) => {
const comments = db.comments.find({});
res.send(comments.map(c => "" + c.text + "
").join(""));
});
// If comment contains:
// Script executes in every visitor's browserPrevent XSS
Escape user input when rendering HTML:
// Sanitize user input before rendering
function escapeHTML(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
// Now user input is safe to render
res.send(comments.map(c => escapeHTML(c.text)).join(""));Or use template engines that auto-escape by default (Handlebars, EJS). If using frameworks like React or Vue, they escape by default:
// React auto-escapes
function Comment({ text }) {
return {text}
; // Safe from XSS
}Cross-Site Request Forgery (CSRF)
CSRF tricks users into making unwanted requests. If a user is logged into their bank, and you trick them into visiting your site, your site can make requests to the bank as if they authorized it.
Example Attack
Prevention: CSRF Tokens
Generate a unique token for each form. The token must be included in the request. Attackers can't know the token, so they can't create valid requests:
// Server: Generate token
app.get("/transfer", (req, res) => {
const token = crypto.randomBytes(32).toString("hex");
req.session.csrfToken = token;
res.send(`
`);
});
// Server: Validate token
app.post("/transfer", (req, res) => {
if (req.body.token !== req.session.csrfToken) {
return res.status(403).send("CSRF token invalid");
}
// Process transfer
});Alternatively, use SameSite cookies:
res.cookie("session", token, {
sameSite: "strict", // Only send on same-site requests
httpOnly: true,
secure: true
});Content Security Policy (CSP)
CSP restricts where scripts and resources can be loaded from. Even if an attacker injects code, CSP limits what they can do:
// Only allow scripts from same origin
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"script-src 'self'; style-src 'self' https://fonts.googleapis.com"
);
next();
});CSP directives:
- script-src: Where scripts can load from
- style-src: Where stylesheets can load from
- img-src: Where images can load from
- connect-src: What URLs fetch/XMLHttpRequest can contact
- frame-ancestors: Which sites can embed this page in an iframe
SQL Injection
Always use parameterized queries. Never concatenate user input into SQL:
// VULNERABLE - never do this:
const query = "SELECT * FROM users WHERE email = " + userInput;
db.run(query); // Attacker can inject SQL!
// SAFE - use parameterized queries:
const query = "SELECT * FROM users WHERE email = ?";
db.run(query, [req.body.email]); // Driver escapes inputAuthentication Best Practices
Hash Passwords
Never store plain text passwords. Use bcrypt:
const bcrypt = require("bcrypt");
// Signup
const hashedPassword = await bcrypt.hash(password, 10);
db.users.insert({ email, password: hashedPassword });
// Login
const user = db.users.findOne({ email });
const valid = await bcrypt.compare(password, user.password);Use HTTPS Only
Always use HTTPS. It encrypts data in transit. Without it, attackers on the network can intercept passwords and tokens.
Set HSTS headers to force HTTPS:
res.setHeader("Strict-Transport-Security", "max-age=31536000");
Secure Cookies
Set appropriate cookie flags:
res.cookie("session", token, {
httpOnly: true, // Not accessible to JavaScript (prevents XSS theft)
secure: true, // Only sent over HTTPS
sameSite: "strict" // Only sent in same-site requests (prevents CSRF)
});Dependency Security
Your dependencies might contain vulnerabilities. Audit regularly:
npm audit // Check for vulnerabilities
npm audit fix // Auto-fix where possibleUse npm security advisories. Pin exact versions. Review dependency updates before upgrading. Remove unused packages.
Environment Variables
Never commit secrets to version control:
// .env (never commit this)
DATABASE_URL=postgresql://user:password@localhost/db
API_KEY=sk-123456789
// Code
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;Add .env to .gitignore. Use a secrets manager in production.
Principle of Least Privilege
Grant users and services only the permissions they need:
- Database users: Run queries on specific tables only, not DROP or DELETE
- API tokens: Scoped to specific actions (read-only, specific endpoints)
- Filesystem: Applications run as unprivileged users
Error Messages
Don't leak information in error messages:
// Bad: reveals database structure
res.status(400).send("Invalid email: no user with that email in users table");
// Good: generic message
res.status(401).send("Invalid credentials");Security Checklist
- Escape user input when rendering HTML
- Use parameterized queries
- Implement CSRF tokens or SameSite cookies
- Set Content-Security-Policy headers
- Hash passwords with bcrypt
- Use HTTPS only
- Set secure cookie flags
- Audit dependencies regularly
- Store secrets in environment variables
- Apply principle of least privilege
- Use generic error messages
Security is a journey, not a destination. Stay informed about vulnerabilities. Keep dependencies updated. Follow the principle of least privilege. Build security into every layer of your application.
🔐 Security Resources
Essential security reading for web devs. Affiliate links support this site.
The Web Application Hacker's Handbook
The definitive guide to web app security. Learn how attackers think.
Hacking APIs (No Starch Press)
Modern API security where most breaches happen. Practical and hands-on.