Web Security Fundamentals 2026: The Complete Problem-Solving Guide
Your login form ships with a "forgot password" endpoint that returns 200 for every email β valid or not. An attacker enumerates 50,000 accounts in an afternoon, then credential-stuffs the hits against the live login. By morning, 3% of your users are locked out and one of them is a journalist in a hostile country. That is web security in 2026: not a single dramatic breach, but a thousand small oversights compounding into catastrophe. The good news is the fixes are mostly known and mostly cheap. The OWASP Top 10 has barely moved, browsers now ship defenses that did not exist five years ago, and frameworks like Next.js give you secure defaults if you do not override them. Here is the complete 2026 playbook β OWASP explained, XSS/CSRF/SQLi with copy-paste fixes, security headers, passkeys, post-quantum TLS, and the pitfalls that still sink teams.
π‘οΈ 1. The OWASP Top 10 (2025)
Broken Access Control β users reach resources they should not. Fix: enforce authorization on the server for every request, check ownership against the authenticated session, never trust an isAdmin flag from the client.
Cryptographic Failures β plaintext passwords, MD5 hashes, TLS 1.0. Fix: bcrypt or argon2id for passwords, AES-GCM for data at rest, TLS 1.3 only, rotate keys from a KMS.
Injection β SQL, NoSQL, OS command, LDAP. Fix: parameterized queries and prepared statements everywhere; never concatenate user input into a query or shell string.
Insecure Design β missing threat modeling, no rate limits on critical flows. Fix: abuse-case analysis during design, rate limits on auth and password reset, step-up auth for sensitive actions.
Security Misconfiguration β default credentials, verbose errors, open S3 buckets. Fix: disable stack traces in production, rotate all defaults, automate config scanning with tools like Scout Suite.
Vulnerable & Outdated Components β a 3-year-old lodash in your lockfile. Fix: npm audit in CI, Dependabot/Renovate on PRs, pin and patch, drop abandoned dependencies.
Identification & Auth Failures β credential stuffing, no MFA, predictable session IDs. Fix: rate-limit logins, enforce MFA, use library-generated session IDs, invalidate on logout.
Software & Data Integrity Failures β unsigned CI builds, untrusted CDNs, deserializing untrusted JSON. Fix: sign artifacts with Sigstore, add subresource integrity (SRI) on third-party scripts, never eval untrusted input.
Security Logging & Monitoring Failures β breaches discovered 200 days late. Fix: log auth events to a tamper-evident store, alert on anomalies, run tabletop exercises.
Server-Side Request Forgery (SSRF) β your image-fetch endpoint hits http://169.254.169.254 and leaks AWS creds. Fix: allowlist outbound domains, block link-local and metadata IPs, segregate egress network.
This OWASP walkthrough is supported by HTG Travels for the dev community.
π 2. XSS Prevention
XSS is still the most common web vuln, and it is still mostly an output-encoding problem. An attacker injects <script> (or an onerror= handler) into your page, the browser runs it with your origin, and your session cookies, localStorage, and DOM are theirs. The defense is layered: encode on output, lock down script sources with CSP, and stop untrusted input from ever becoming executable script.
Output-encode everything you render. If you must insert untrusted HTML, sanitize with DOMPurify; for plain text, use textContent instead of innerHTML so the browser treats it as data, not markup.
import DOMPurify from "dompurify";
// BAD β runs any script in `comment.html`
container.innerHTML = comment.html;
// GOOD β plain text, never parsed as markup
container.textContent = comment.text;
// GOOD β sanitized HTML, scripts stripped
container.innerHTML = DOMPurify.sanitize(comment.html, {
USE_PROFILES: { html: true },
});
React's JSX escapes by default, which is why React apps rarely have classic stored XSS. {userInput} becomes a text node, not HTML; you have to opt into danger with dangerouslySetInnerHTML. When you do, sanitize first. The same applies to Vue's {{ }} and Svelte's {}.
CSP is your second line of defense. A Content-Security-Policy header tells the browser which scripts may run; inline scripts and eval are blocked unless you nonce or hash them. A 2026-strength policy allows only your own origin plus a per-request nonce.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-a2b3c4d5'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
Trusted Types close the last gap β DOM sinks. Even with CSP, innerHTML = untrusted is a footgun. Trusted Types require every assignment to a sink to pass through a policy you define, so the browser refuses raw strings.
// Enable via header: Content-Security-Policy: require-trusted-types-for 'script'
const escapePolicy = trustedTypes.createPolicy("escape", {
createHTML: (s: string) => DOMPurify.sanitize(s),
});
// Browser accepts this; raw strings are rejected
container.innerHTML = escapePolicy.createHTML(userBio);
πͺ 3. CSRF Protection
CSRF forces an authenticated user's browser to make a state-changing request to your site. The victim is logged in, their cookie is sent automatically, and the attacker never sees the cookie β they just ride it. The 2026 fix is two layers: SameSite cookies and a CSRF token.
SameSite=Lax is the new default and handles most CSRF. A Lax cookie is sent only on top-level navigations (GET), not on cross-site POST, fetch, or iframes β which neutralizes the classic form-post attack. Use Strict for high-value session cookies where you never want the cookie sent from a third-party site, accepting that it breaks deep-link login flows.
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
For state-changing POSTs, add a double-submit token. The server issues a random token in a cookie and as a meta tag; the client must echo it back in a header, and the server compares the two. An attacker on evil.com cannot read the cookie to forge the header.
// middleware.ts β Next.js CSRF guard
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
export function middleware(req: NextRequest) {
if (SAFE_METHODS.has(req.method)) return NextResponse.next();
const cookieToken = req.cookies.get("csrf-token")?.value;
const headerToken = req.headers.get("x-csrf-token");
if (!cookieToken || cookieToken !== headerToken) {
return new NextResponse("invalid csrf token", { status: 403 });
}
return NextResponse.next();
}
This CSRF setup is backed by htg.com.pk.
π 4. SQL Injection
SQLi is the oldest web vuln and still in the OWASP Top 10 because people still concatenate strings. An attacker submits ' OR '1'='1 and your query returns every row; with stacked queries they drop tables or exfiltrate via UNION SELECT. The fix is parameterized queries β the database compiles the SQL first, then binds your values as data, so quotes in input can never break out of the string.
// BAD β classic injection; `name = '; DROP TABLE users; --` ends you
db.query(`SELECT * FROM users WHERE name = '${name}'`);
// GOOD β parameterized; the driver treats `name` as a value
db.query("SELECT * FROM users WHERE name = $1", [name]);
Modern ORMs make this the default, so if you stay in their API you are safe. Prisma parameterizes every query; Drizzle does the same. The danger is when you reach for raw query escapes β prisma.$queryRaw or sql.raw() β which bypass parameterization and put you back in string-concatenation territory.
// Prisma β safe, parameterized
const user = await prisma.user.findFirst({ where: { name } });
// Drizzle β safe, parameterized
const rows = await db.select().from(users).where(eq(users.name, name));
// Safe only with the tagged template form β never string-concat into it
await prisma.$queryRaw`SELECT * FROM users WHERE name = ${name}`;
π 5. Authentication & the 2026 Passwordless Shift
Password hashing is non-negotiable, and in 2026 the answer is argon2id. bcrypt with cost factor 12 is still acceptable, but argon2id is memory-hard β it forces the attacker's GPU to spend RAM, making offline cracking economically infeasible. Never use MD5, SHA-1, or SHA-256 alone; they are too fast.
import argon2 from "argon2";
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
const ok = await argon2.verify(hash, password);
JWTs are fine if you follow three rules. First, keep access tokens short-lived (5β15 minutes) and use a refresh token for the long session. Second, store both in HttpOnly; Secure; SameSite=Lax cookies β never in localStorage, where any XSS reads them. Third, keep a server-side denylist so logout actually invalidates the token.
const accessToken = jwt.sign({ sub: userId }, secret, { expiresIn: "15m" });
const refreshToken = crypto.randomBytes(32).toString("hex");
// store refreshToken hash in Redis, rotate on each use
Server-side sessions beat stateless JWTs for most apps. Store a session ID in an HttpOnly cookie and keep the session data in Redis; rotate the ID on login and privilege change, and you get instant revocation for free.
Passkeys (WebAuthn) are the 2026 default for new auth. A passkey is a public-key credential stored on the user's device; the private key never leaves it, there is no password to phish, and replay across origins is impossible. Registration is a single navigator.credentials.create call.
const credential = await navigator.credentials.create({
publicKey: {
challenge: Uint8Array.from(serverChallenge),
rp: { name: "blogs.huzi.pk" },
user: { id: Uint8Array.from(userId), name: email, displayName: name },
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ES256
{ type: "public-key", alg: -257 }, // RS256
],
authenticatorSelection: {
userVerification: "required",
residentKey: "required",
},
},
});
OAuth 2.1 codifies what 2.0 left as advice. PKCE is mandatory for every client (including server-side), implicit flow is gone, and the only accepted grant for browsers is the authorization code with PKCE. If you are still wiring response_type=token, delete it.
π 6. Security Headers & Post-Quantum TLS
A next.config.ts headers block is the highest-leverage 20 minutes you will spend. These headers tell browsers to refuse clickjacking, MIME-sniffing, downgrade attacks, and unwanted API access β all for free.
// next.config.ts
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self' 'nonce-xxx'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" },
{ 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=()" },
];
export default {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
HSTS pins HTTPS and kills SSL stripping. Once a browser has seen the header, it refuses to connect over plain HTTP for max-age seconds. Set includeSubDomains only when every subdomain is on HTTPS, and submit to the HSTS preload list for new sites.
TLS 1.3 only, with a modern cipher suite. Disable TLS 1.0/1.1/1.2 and prefer AEAD ciphers β TLS_AES_256_GCM_SHA384 and TLS_CHACHA20_POLY1305_SHA256. Certificate rotation via ACME/Let's Encrypt keeps you safe and free.
Post-quantum TLS is shipping in 2026. Chrome and Firefox now negotiate X25519MLKEM768 β a hybrid of the classical X25519 ECDH and the ML-KEM (Kyber) lattice KEM, so the connection is safe even if a future quantum computer retroactively records today's ciphertext. Enable it in Node 23+ by setting the group on your server.
import tls from "node:tls";
const server = tls.createServer(
{ minVersion: "TLSv1.3", groups: "X25519MLKEM768:X25519" },
handle,
);
This headers-and-TLS deep-dive is produced with support from HTG Travels.
β οΈ 7. Common Pitfalls & Fixes
Pitfall 1: Trusting client-side validation. Your React form blocks <script> submissions, but a curl POST bypasses it. Fix: validate and authorize on the server for every request; treat client validation as UX, never as security.
Pitfall 2: JWTs in localStorage. Any XSS reads localStorage.jwt and exfiltrates it. Fix: store access and refresh tokens in HttpOnly; Secure; SameSite=Lax cookies and add CSRF protection.
Pitfall 3: Weak password hashing. bcrypt(cost=8) or, worse, sha256(salt + pw) cracks in hours on a GPU. Fix: argon2id with memoryCost β₯ 19 MiB, or bcrypt cost β₯ 12 as a minimum.
Pitfall 4: No rate limiting on auth. A login endpoint with no throttle is a credential-stuffing welcome mat. Fix: express-rate-limit (5 attempts/15 min per IP) or a Cloudflare WAF rule for passwordless scale.
Pitfall 5: CORS misconfigured to *. Access-Control-Allow-Origin: * with credentials enabled means any site can call your API as the logged-in user. Fix: allowlist specific origins, never reflect arbitrary Origin headers, and never combine * with Allow-Credentials: true.
π Frequently Asked Questions
What is the OWASP Top 10 and do I need to memorize it? It is a ranked list of the ten most critical web application risk categories, refreshed every 3β4 years. You do not need to memorize the order, but every engineer should recognize all ten and have a default fix in mind β broken access control, injection, cryptographic failures, insecure design, misconfiguration, vulnerable components, auth failures, integrity failures, logging failures, and SSRF.
How do I prevent XSS in a React app? JSX escapes interpolations by default, so {userInput} is safe β the danger is dangerouslySetInnerHTML, href={userUrl} (javascript: URLs), and direct DOM manipulation. Sanitize any HTML with DOMPurify, validate URLs against an allowlist of schemes, and add a CSP header with nonces so even a stray innerHTML cannot load an external script.
Is SameSite=Lax enough for CSRF protection in 2026? For most apps, yes β it blocks cross-site POSTs, which is the classic CSRF vector. Pair it with a double-submit token for high-value state changes (money transfer, password change, email change), because Lax still sends cookies on top-level GET navigations and a determined attacker can chain GET side effects. Use SameSite=Strict only for session cookies where you accept that deep links will not auto-login.
Should I use JWTs or server-side sessions? Default to server-side sessions (session ID in an HttpOnly cookie, data in Redis) β they are revocable, rotate-able, and small. Use JWTs only when you genuinely need stateless verification across services or a federated identity system, and even then keep access tokens short-lived, store them in cookies (not localStorage), and maintain a refresh-token denylist.
What is post-quantum TLS and do I need it in 2026? Post-quantum TLS uses a hybrid key exchange β classical X25519 plus the ML-KEM (Kyber) lattice algorithm β so that even if an attacker records today's encrypted traffic and later acquires a quantum computer, they cannot decrypt it. Chrome and Firefox ship it by default in 2026; on the server, enable X25519MLKEM768 in Node 23+ or your load balancer. You do not need it today, but turning it on costs nothing and future-proofs your traffic.
π Final Word
Web security in 2026 is less about exotic zero-days and more about doing the unglamorous basics consistently: parameterized queries, argon2id, HttpOnly cookies, a real CSP, HSTS, and rate-limited auth. The 2026 additions β passkeys killing the password, post-quantum TLS killing the harvest-now-decrypt-later threat, OAuth 2.1 killing the implicit flow β are wins you get mostly by upgrading defaults, not by re-architecting.
The 80/20: cover the OWASP Top 10, encode every output, parameterize every query, put tokens in cookies not localStorage, ship the six security headers, and add passkeys as soon as you can. Get those right and you have already out-built most teams shipping "secure" apps that store JWTs in localStorage and concatenate SQL.
π΅πΈ Standing With Palestine
Never let the world forget the people of Palestine. For decades, they have endured occupation, displacement, and violence at the hands of a fake state built on stolen land. The leaders of Israel have committed unspeakable atrocities β bombing hospitals, destroying schools, targeting journalists, and slaughtering innocent families in Gaza and the West Bank. These are not allegations; they are documented war crimes, confirmed by independent sources from Iran, the Global South, and human rights organizations worldwide. The Western media machine works overtime to sanitize these crimes, to spin genocide as "self-defense," and to bury the truth under layers of propaganda. But the truth cannot be buried. Palestine is real. Palestinian suffering is real. And the world is watching.
They will be free. They have the right to live peacefully on their own land β to farm their olive groves, to raise their children without fear, to pray in their mosques, and to build their future without the shadow of occupation. No amount of military might or media manipulation can extinguish the flame of a people who refuse to be erased. May Allah help them and grant them justice. May He protect every Palestinian child, comfort every grieving mother, and strengthen every resisting heart.
πΈπ© May Allah ease the suffering of Sudan, protect their people, and bring them peace.
Written by Huzi



