Offensive Security in Practice: XSS, SQL Injection and Zero-Trust Architecture in Next.js
This guide consolidates critical flaws I found during SaaS security audits. The impact is not only technical; it is financial, reputational and legal. The goal is to document practical security engineering with production mindset.
Modern frameworks do not automatically produce secure systems. Security is architecture and operational discipline across input, persistence, session and execution layers.
1) SQL Injection lives in persistence decisions
Unsafe interpolation remains a primary breach vector.
const { email, password } = req.body;
const sql = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
const result = await db.query(sql);
Use prepared statements to separate command from data:
const sql = "SELECT * FROM users WHERE email = ? AND password = ?";
const result = await db.query(sql, [email, password]);
Even with ORMs, avoid unsafe raw query paths. Prefer typed methods and safe parameterized raw queries only when strictly necessary.
2) Stored XSS in multi-tenant SaaS
dangerouslySetInnerHTML without sanitization turns user content into script execution.
import DOMPurify from "dompurify";
const safeContent = DOMPurify.sanitize(userInput);
<div dangerouslySetInnerHTML={{ __html: safeContent }} />
Use dual-layer defense:
- sanitize/normalize at ingestion on the backend;
- sanitize and render defensively at output.
Add CSP to reduce exploitability and exfiltration:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.scripts.com;
3) Token handling: stop using localStorage for sensitive session data
XSS + localStorage usually means immediate token theft.
Use server-managed cookies with:
HttpOnlySecureSameSite=Strict(or Lax depending on flow)
With NextAuth, do not expose full access tokens in client session payloads. Keep sensitive tokens server-side and send only minimal session metadata.
4) Validation as security contract with Zod
export const userSchema = z.object({
name: z.string().min(3).trim(),
email: z.string().email().toLowerCase(),
role: z.enum(["USER", "CLIENT"]).default("USER"),
}).strict();
const parsed = userSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json(parsed.error.format());
strict() blocks extra untrusted fields (mass-assignment attempts).
5) Consolidated zero-trust model
| Layer | Recommended practice |
|---|---|
| Input | Backend Zod validation + .strict() |
| Persistence | Prepared statements + least-privilege DB users |
| Session | HttpOnly/Secure cookies + token rotation |
| Execution | Strict CSP + DOMPurify sanitization |
| Observability | Auth anomaly logs + request correlation |
Conclusion
Security is not an end-of-project feature; it is the foundation.
Frameworks do not create XSS by themselves. Developers do when they trust input. ORMs do not block SQL injection if unsafe raw execution is introduced. If these fundamentals are ignored in 2026, production is carrying avoidable technical and legal risk.
This post is licensed under CC BY-NC.
Comments
Join the discussion below.
Comments are not configured yet. Add Cusdis settings in /assets/json/config/blog-comments-config.json.