XSS — Cross-Site Scripting · CSRF & Authentication · SQL Injection · HTTPS, Secrets & Headers
XSS lets attackers inject scripts that run in your users' browsers. Learn the three flavors, how to escape output, and how Content Security Policy gives defense in depth. Protect authenticated endpoints from CSRF. Use SameSite cookies, CSRF tokens, and understand the trade-offs between session cookies and JWT tokens. SQL injection is decades old and still happening. Always use prepared statements, validate at boundaries, and treat ORM queries with the same care. Encrypt traffic with HTTPS, keep secrets out of git, and harden HTTP responses with the right headers (CSP, HSTS, X-Frame-Options).
What you'll learn
- 1Distinguish stored, reflected, and DOM XSS
- 2Always escape output (default in React/Vue/Angular)
- 3Avoid innerHTML / dangerouslySetInnerHTML
- 4Configure a Content Security Policy
- 5Explain CSRF in one sentence
- 6Block CSRF with SameSite=Lax/Strict cookies
- 7Use CSRF tokens when SameSite is not enough
- 8Compare session cookies vs JWT (and pick session for new apps)
- 9Recognize a vulnerable query in 5 seconds
- 10Always use parameterized queries
- 11Validate input at the API boundary
- 12Use ORMs safely (no raw template strings)
- 13Serve every page over HTTPS
- 14Use environment variables for secrets, never commit .env
- 15Apply HSTS to force HTTPS
- 16Use helmet in Express for sensible defaults
XSS — Cross-Site Scripting
XSS lets attackers inject scripts that run in your users' browsers. Learn the three flavors, how to escape output, and how Content Security Policy gives defense in depth.
1. The Three Flavors
- Stored — payload saved on the server (e.g., in a comment) and served to other users
- Reflected — payload in a URL/query that the server reflects back into HTML
- DOM — JavaScript on the client builds HTML from untrusted input
2. Always Escape Output
// VULNERABLE
el.innerHTML = `<p>Hello, ${userInput}</p>`;
// If userInput = '<img src=x onerror="alert(1)">' — game over
// SAFE
el.textContent = `Hello, ${userInput}`;
// Or React: {userInput} is auto-escapedAvoid React's dangerouslySetInnerHTML unless you sanitize input with a library like DOMPurify first.
3. Sanitize When You Must Render HTML
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(richHtml);
el.innerHTML = clean;4. Content Security Policy (CSP)
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data: https:Even if an attacker injects a <script>, the browser refuses to run it unless it matches your CSP. Defense in depth.
CSRF & Authentication
Protect authenticated endpoints from CSRF. Use SameSite cookies, CSRF tokens, and understand the trade-offs between session cookies and JWT tokens.
1. What is CSRF?
An attacker tricks a logged-in user's browser into firing a request to your site, riding on their cookies. The user did not intend it.
<!-- attacker.com -->
<img src="https://yourbank.com/transfer?to=evil&amount=1000">2. SameSite Cookies (First Line of Defense)
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/- Lax — cookie not sent on cross-site POST (good default)
- Strict — cookie not sent on any cross-site request
- None — only with Secure; needed for embedded contexts
3. CSRF Tokens
Render a per-session random token in your form. The server requires it on submit. The attacker's site cannot read it (Same-Origin Policy).
// Express + csurf
import csurf from "csurf";
app.use(csurf({ cookie: true }));
app.get("/form", (req, res) => {
res.render("form", { csrfToken: req.csrfToken() });
});
app.post("/transfer", (req, res) => { /* csurf validates automatically */ });4. Session Cookies vs JWT
| Session cookie | JWT (Authorization header) | |
|---|---|---|
| Storage | Server-side (Redis/DB) | Client-side (token contains data) |
| Revoke | Easy (delete the row) | Hard — must check blocklist |
| Size | Tiny | Large (whole user info) |
| XSS | HttpOnly cookie cannot be stolen | localStorage tokens can be stolen |
| Best for | Most web apps | Stateless APIs, mobile clients |
Default to session cookies for traditional web apps. JWT shines when you cannot rely on cookies (mobile, B2B APIs).
SQL Injection
SQL injection is decades old and still happening. Always use prepared statements, validate at boundaries, and treat ORM queries with the same care.
1. The Classic Example
// VULNERABLE — string concatenation
const sql = `SELECT * FROM users WHERE email = '${email}'`;
// email = "x' OR '1'='1" — returns ALL rows
// email = "x'; DROP TABLE users; --" — drops the table2. Prepared Statements (Always)
// SAFE — placeholder + parameter
const stmt = db.prepare("SELECT * FROM users WHERE email = ?");
const user = stmt.get(email);
// PostgreSQL with pg
await pool.query("SELECT * FROM users WHERE email = $1", [email]);
// MySQL with mysql2
await pool.execute("SELECT * FROM users WHERE email = ?", [email]);Parameters are not string substitution — the driver sends them as separate data, so they can never be SQL.
3. ORMs
// Prisma — safe by design
await prisma.user.findUnique({ where: { email } });
// Raw query in Prisma — STILL safe with $queryRaw + tagged template
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
// DANGEROUS — string interpolation defeats the protection
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);4. Defense in Depth
- Validate input shape (Zod, Joi) — reject obvious garbage at the boundary
- Use least-privileged DB users (the app user should not be able to DROP tables)
- Log and rate-limit suspicious queries
- Run a SAST scanner in CI (e.g., Semgrep)
HTTPS, Secrets & Headers
Encrypt traffic with HTTPS, keep secrets out of git, and harden HTTP responses with the right headers (CSP, HSTS, X-Frame-Options).
1. HTTPS Everywhere
- Vercel / Netlify give you HTTPS automatically
- On a VPS, use Caddy or Certbot (Let's Encrypt) — free certificates
- Set HSTS to force HTTPS on revisits
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload2. Secrets in .env
# .env (never committed)
DATABASE_URL=postgres://...
JWT_SECRET=...
STRIPE_KEY=...
# .gitignore
.env
.env.localimport "dotenv/config";
const dbUrl = process.env.DATABASE_URL;If you ever commit a secret, ROTATE it immediately. Removing the commit does not help — git history is forever.
3. Security Headers (Express + helmet)
import helmet from "helmet";
app.use(helmet()); // sane defaults: CSP, HSTS, frameguard, noSniff, etc.| Header | Effect |
|---|---|
| Content-Security-Policy | Limits what scripts/styles/etc. can run |
| Strict-Transport-Security | Force HTTPS |
| X-Content-Type-Options: nosniff | Stop MIME sniffing |
| X-Frame-Options: DENY | Prevent clickjacking |
| Referrer-Policy | Limit Referer leaks |
4. Final Checklist Before Shipping
- HTTPS on every endpoint
- Secrets in env vars, never in code
- Strong password hashing (bcrypt, argon2)
- Rate-limit login and signup endpoints
- Log security events (failed logins, role changes)
- Keep dependencies updated (npm audit)
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy