Skip to content

Web Security for Developers: The Essentials You Can't Skip

The security vulnerabilities that appear most often in web applications, explained with real code examples and concrete fixes every developer should know.

3 min read
Circuit-etched shield with a padlock above three labelled threats: SQL injection, cross-site scripting and broken access control.

Security Is Not a Sprint at the End

Most security vulnerabilities aren't sophisticated attacks. They're basic mistakes made under deadline pressure — missing input validation, leaked secrets, forgotten authorization checks. This guide covers the practical fundamentals every developer should internalize.

Vulnerability 1: SQL Injection

Still the most common critical vulnerability in web applications, despite being solved decades ago.

tstypescript
// ❌ Never concatenate user input into SQL
async function getUser(username: string) {
  // If username = "admin' OR '1'='1" — returns all users
  const query = `SELECT * FROM users WHERE username = '${username}'`;
  return db.query(query);
}
 
// ✅ Always use parameterized queries
async function getUser(username: string) {
  return db.query("SELECT * FROM users WHERE username = $1", [username]);
}
 
// ✅ Or use a query builder / ORM
async function getUser(username: string) {
  return prisma.user.findUnique({ where: { username } });
}

Parameterized queries are non-negotiable. There is no safe way to concatenate user input into SQL.

Vulnerability 2: Cross-Site Scripting (XSS)

XSS allows attackers to inject scripts into your pages that run in other users' browsers — stealing sessions, exfiltrating data, or hijacking accounts.

tsxtsx
// ❌ Never render raw HTML from untrusted sources
function Comment({ content }: { content: string }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
 
// If content = "<script>document.cookie</script>" — you've been XSS'd
 
// ✅ React escapes by default — use it
function Comment({ content }: { content: string }) {
  return <div>{content}</div>; // Safe — React escapes HTML entities
}
 
// ✅ If you must render HTML, sanitize first
import DOMPurify from "dompurify";
 
function RichContent({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["p", "b", "i", "em", "strong", "a"],
    ALLOWED_ATTR: ["href", "title"],
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Also set a strict Content Security Policy to limit what scripts can execute even if XSS does occur.

Vulnerability 3: Broken Access Control

Authorization bugs are subtle because the code usually works — it just works for the wrong users.

tstypescript
// ❌ Missing authorization check — any authenticated user can delete any comment
app.delete("/api/comments/:id", authenticate, async (req, res) => {
  await db.comment.delete({ where: { id: req.params.id } });
  res.json({ success: true });
});
 
// ✅ Verify ownership before mutation
app.delete("/api/comments/:id", authenticate, async (req, res) => {
  const comment = await db.comment.findUnique({
    where: { id: req.params.id },
  });
 
  if (!comment) {
    return res.status(404).json({ error: "Not found" });
  }
 
  if (comment.authorId !== req.user.id) {
    return res.status(403).json({ error: "Forbidden" });
  }
 
  await db.comment.delete({ where: { id: req.params.id } });
  res.json({ success: true });
});

For complex applications, use a dedicated authorization library (Casbin, Permit.io, or a custom RBAC implementation) rather than ad-hoc if checks scattered throughout the codebase.

Vulnerability 4: Insecure Secret Management

Secrets in code are eventual leaks. Every API key committed to a repository should be considered compromised.

shbash
# ❌ Hard-coded secrets
DATABASE_URL="postgresql://admin:password123@prod-db/app"
STRIPE_SECRET_KEY="sk_live_..."
 
# ✅ Environment variables — never committed
echo ".env.local" >> .gitignore
echo ".env*.local" >> .gitignore
tstypescript
// ✅ Validate secrets at startup, fail fast if missing
const requiredEnvVars = [
  "DATABASE_URL",
  "JWT_SECRET",
  "STRIPE_SECRET_KEY",
] as const;
 
for (const key of requiredEnvVars) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
}

Use a secrets manager (AWS Secrets Manager, Doppler, HashiCorp Vault) for production. Rotate any key that was ever committed to a repository, even if the commit was reverted.

Vulnerability 5: Weak Authentication

JWT secrets with default values, sessions that never expire, passwords stored in plaintext — these are the authentication patterns that lead to breaches.

tstypescript
// ❌ Weak JWT configuration
const token = jwt.sign({ userId }, "secret"); // No expiry, weak secret
 
// ✅ Proper JWT configuration
const token = jwt.sign(
  { userId, iat: Math.floor(Date.now() / 1000) },
  process.env.JWT_SECRET!, // Strong, random secret from env
  {
    expiresIn: "15m", // Short-lived access tokens
    issuer: "myapp.com",
    audience: "api.myapp.com",
  },
);
 
// ✅ Password hashing — always use bcrypt or argon2
import { hash, verify } from "argon2";
 
async function hashPassword(password: string): Promise<string> {
  return hash(password, {
    type: argon2.argon2id,
    memoryCost: 19456,
    parallelism: 1,
    timeCost: 2,
  });
}
 
async function verifyPassword(
  hash: string,
  password: string,
): Promise<boolean> {
  return verify(hash, password);
}

Security Headers Checklist

tstypescript
// next.config.ts — minimum security headers for any web app
const securityHeaders = [
  { key: "X-Frame-Options", value: "DENY" },
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  {
    key: "Permissions-Policy",
    value: "camera=(), microphone=(), geolocation=()",
  },
  {
    key: "Content-Security-Policy",
    value: [
      "default-src 'self'",
      "script-src 'self' 'nonce-{NONCE}'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.myapp.com",
    ].join("; "),
  },
];

Run your application through securityheaders.com and OWASP ZAP before launch. Add these checks to your CI pipeline.

Security is not about being paranoid. It's about understanding how your application handles untrusted data and making sure every path is intentional.

Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX