← Back to Articles

Web Security Essentials

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 browser

Prevent XSS

Escape user input when rendering HTML:

// Sanitize user input before rendering
function escapeHTML(str) {
  return str
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

// 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:

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 input

Authentication 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 possible

Use 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:

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

  1. Escape user input when rendering HTML
  2. Use parameterized queries
  3. Implement CSRF tokens or SameSite cookies
  4. Set Content-Security-Policy headers
  5. Hash passwords with bcrypt
  6. Use HTTPS only
  7. Set secure cookie flags
  8. Audit dependencies regularly
  9. Store secrets in environment variables
  10. Apply principle of least privilege
  11. 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.