All articles
Security Architecture OWASP Best Practices

Security Attacks Every Engineer Should Know: A Field Guide to the Techniques Attackers Actually Use

Palakorn Voramongkol
April 15, 2026 22 min read

“A structural, named-technique-by-named-technique walkthrough of the attacks a modern web application has to defend against — injection, authentication flaws, XSS, CSRF, IDOR, SSRF, deserialization, race conditions, supply-chain attacks, and more — with how each works and what to do about it.”

Security Attacks Every Engineer Should Know: A Field Guide to the Techniques Attackers Actually Use

The best way to write secure software is to know how attackers break insecure software. Not as a recipe — as a mental model. Once you’ve seen how SQL injection works, you stop concatenating user input into queries everywhere, not just in the function you happened to be thinking about. Once you understand IDOR, you stop trusting the ID in the URL anywhere. The attack names become shorthand for whole families of mistakes.

This post is a field guide: the named techniques that appear in real bug bounties, CVEs, and post-mortems, grouped by what they target. For each I’ll explain what it is, the attacker’s mental model, the code smell that enables it, and the mitigation.

This is defensive security content. Understanding the attack is step one of building systems that don’t fall to it.

How Attackers Think

Before the list, a framing that will save you from looking up every vulnerability when a new one drops.

  1. Every boundary is a trust boundary. The browser → server boundary is the obvious one, but also: user → admin, tenant → tenant, microservice A → microservice B, your code → a dependency, production → staging, request → stored data. Most vulnerabilities are “data flowed across a boundary and the receiver trusted it too much”.

  2. Attackers compose primitives. A single “low-severity” bug is rarely the end game. A minor info leak + a weak auth check + a CSRF gap chain into account takeover. Classifying a bug as “low” in isolation is often wrong.

  3. Attackers don’t play by the UI. They call your endpoints directly, replay requests, race them, rename fields, strip CSRF tokens. Anything you rely on the UI to enforce can be bypassed. Validation must happen on the server.

  4. The weakest link wins. A perfectly-hardened login endpoint is useless if the password-reset endpoint is weak, or if support staff can read the password-reset tokens in logs, or if the dependency that parses uploaded PDFs has an RCE. Your security is the minimum of every path in.

Injection Family

All forms of injection share one shape: data the user controls gets interpreted as instructions by a downstream parser.

sequenceDiagram
  participant A as Attacker
  participant S as Server
  participant DB as Database
  A->>S: POST /login {email: "' OR 1=1 --", password: "x"}
  S->>DB: SELECT * FROM users WHERE email='' OR 1=1 --' AND ...
  Note over DB: Input becomes SQL<br/>WHERE clause bypassed
  DB-->>S: First row in users table
  S-->>A: Logged in as first user (often admin)

SQL Injection (SQLi)

  • What: user input is concatenated into a SQL query.
  • Classic example: "SELECT * FROM users WHERE email = '" + userInput + "'". Input ' OR 1=1 -- turns it into SELECT * FROM users WHERE email = '' OR 1=1 --'.
  • Variants: classic in-band (error-based, UNION-based), blind (boolean-based, time-based SLEEP(5)), out-of-band (exfiltration via DNS).
  • Defence: parameterised queries / prepared statements, always. Never string-concatenate SQL. ORMs that use parameters by default (Prisma, Drizzle, SQLAlchemy) make this easy. If you must interpolate an identifier (table/column name), allow-list it against a fixed set — never an arbitrary string.

NoSQL Injection

  • What: same idea, different grammar. In MongoDB: { email: userInput } where userInput is { "$ne": null } returns every document.
  • Defence: validate that fields are the expected type (string, not object). Reject request bodies with operator keys ($eq, $ne, $gt) unless your API genuinely accepts them.

OS Command Injection

  • What: user input reaches a shell. exec("convert " + filename + " out.png") with filename "a.png; rm -rf /".
  • Defence: don’t spawn shells. Use the argv-array form (spawn('convert', ['file.png', 'out.png'])) so the filename is one argument — no shell parses it.

LDAP, XPath, XQuery, Template Injection (SSTI)

Different query languages, same pattern. Template injection is especially nasty — {{ 7*7 }} rendering as 49 means the attacker has remote code execution in many template engines (Jinja, Twig, Velocity).

  • Defence: never pass user input as the template — only as variables bound into a template written by you.

CRLF Injection / Header Injection

  • What: user input contains \r\n (carriage return, line feed) and gets inserted into an HTTP header. Attacker can inject a new header or split a response.
  • Defence: strip or reject \r and \n in any data used in headers — especially redirects, Set-Cookie values, and CORS origins.

Authentication & Session Family

Where attackers take over someone’s account without knowing the password.

sequenceDiagram
  participant A as Attacker
  participant V as Victim
  participant S as Server
  A->>S: GET / (obtain fresh session SID=abc)
  A->>V: Phishing link https://site.com/?SID=abc
  V->>S: GET /?SID=abc (browser stores cookie)
  V->>S: POST /login with credentials
  S-->>V: Logged in — SID=abc now authenticated
  A->>S: GET /account cookie: SID=abc
  S-->>A: Victim's session (session fixation)

Credential Stuffing

  • What: attacker takes a database of (email, password) pairs leaked from another site and tries them all on your login endpoint. A meaningful fraction of users reuse passwords.
  • Defence: rate-limit logins aggressively per account and per IP; require 2FA/MFA; use captchas after N failures; subscribe to Have I Been Pwned’s password API to reject known-breached passwords at signup.

Brute Force

  • What: automated guessing of passwords, tokens, or OTPs.
  • Defence: rate limiting, exponential backoff, account lockout (careful — lockout can be weaponised for DoS), long random tokens (128+ bits of entropy), short OTP validity (5 min) with limited retries (5).

Session Hijacking

  • What: attacker obtains a valid session cookie/token by sniffing (on insecure networks), stealing via XSS, or stealing via physical access.
  • Defence: TLS everywhere (HSTS preload), Secure; HttpOnly; SameSite=Lax on cookies, short session lifetimes with refresh, rotate sessions on privilege change (login, password reset).

Session Fixation

  • What: attacker forces you to use a session ID they already know (via a crafted URL). When you log in with it, they share the session.
  • Defence: regenerate the session ID on login. This is a one-liner in every session framework and is often forgotten.

JWT Attacks

  • What: several sharp edges in JSON Web Tokens:

  • alg: none: old libraries accept none as a valid algorithm — no signature verification.

  • Algorithm confusion: a server expects RS256 (public/private key) but an attacker sends a token signed with HS256 using the public key as the secret.

  • Weak HMAC secret: brute-force HS256 with a 6-character secret.

  • kid header injection: kid (key ID) is user-controlled — if used as a file path or SQL query, you have file read or SQLi.

  • Defence: explicitly specify expected algorithm in verification code (don’t trust the header); use strong secrets (32+ random bytes) or asymmetric keys; validate iss, aud, exp, nbf, sub.

Password Reset Poisoning

  • What: the reset email link is built from request.getHost(). Attacker sends a reset request for your email but with Host: attacker.com. You click the link, which points at attacker.com with your valid reset token.
  • Defence: never trust the Host header. Use a hardcoded SITE_URL env var when generating absolute links.

Account Enumeration

  • What: “Email not found” vs “Password incorrect” tells the attacker which emails exist.
  • Defence: return identical responses and timings for “user doesn’t exist” vs “wrong password”. On password reset, return “we sent an email if the address is on file” regardless of whether it exists.

Cross-Site Family

Attacks that abuse the browser’s willingness to execute scripts or send cookies from one origin while visiting another.

sequenceDiagram
  participant V as Victim (logged in at target.com)
  participant A as Attacker's site
  participant T as target.com
  V->>A: Visits evil.com
  A-->>V: <form action="https://target.com/transfer"> auto-submit
  V->>T: POST /transfer?to=attacker<br/>Cookie: session=... (attached automatically)
  T-->>V: 200 — transfer succeeded
  Note over T: No CSRF token, no Origin check —<br/>server cannot tell the user did not mean it

XSS (Cross-Site Scripting)

  • What: attacker’s JavaScript runs in a victim’s browser, in the context of your site. Three flavours:

  • Stored XSS: payload is saved in your database (a user’s bio, a comment) and served to other users.

  • Reflected XSS: payload is in the URL (?q=<script>) and echoed unescaped into the response.

  • DOM-based XSS: JS on the page reads from location.hash or document.referrer and injects it as HTML without sanitisation.

  • Defence:

  • Output encoding by default: frameworks like React, Vue, and modern template engines escape HTML by default. Don’t undo it with dangerouslySetInnerHTML / v-html unless the input is from a known-safe source.

  • Content Security Policy (CSP): default-src 'self'; script-src 'self' 'nonce-xxx' blocks inline scripts and external sources the attacker might inject.

  • Sanitise HTML you accept: libraries like DOMPurify for user-provided rich text.

  • Cookie flags: HttpOnly prevents JS reading cookies — mitigates session theft even if XSS occurs.

CSRF (Cross-Site Request Forgery)

  • What: attacker’s site makes your logged-in browser send a request to your site (<img src="https://bank.com/transfer?to=attacker&amount=1000">). Browser attaches your cookies automatically.
  • Defence:
  • SameSite cookies: SameSite=Lax (default in modern browsers) blocks most cross-site cookie attachment. SameSite=Strict for auth cookies.
  • CSRF tokens: for state-changing form posts, include a per-session random token that the attacker can’t guess.
  • Origin/Referer check on sensitive endpoints.
  • Use POST/PUT/DELETE for state changes, not GET.

Clickjacking

  • What: attacker frames your site inside an invisible iframe and tricks users into clicking buttons they can’t see.
  • Defence: X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'self'.

XS-Leaks (Cross-Site Leaks)

  • What: side-channel info leaks across origins — e.g. an attacker’s page can measure how long your page takes to respond and infer whether the user is logged in or has a certain permission.
  • Defence: Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp, avoid leaking timing differences based on auth state.

Access Control Family

Resource fetches where the server forgets to check that the logged-in user is allowed to touch that specific record.

sequenceDiagram
  participant A as Attacker (user id=42)
  participant S as Server
  participant DB as Database
  A->>S: GET /api/invoices/42 (own invoice)
  S->>DB: SELECT * FROM invoices WHERE id=42
  DB-->>S: invoice #42
  S-->>A: 200 OK
  A->>S: GET /api/invoices/100 (belongs to user 99)
  S->>DB: SELECT * FROM invoices WHERE id=100
  Note over S: No AND user_id = 42 check
  DB-->>S: invoice #100
  S-->>A: 200 OK — leak

IDOR (Insecure Direct Object Reference)

  • What: URL like /api/invoices/1234. Attacker changes to /api/invoices/1235 and gets someone else’s invoice. Because the server only checked “are you logged in?”, not “do you own this resource?”.
  • Defence: every resource fetch must authorise the current user against that specific resource. WHERE id = ? AND user_id = ?. On list endpoints, always scope to the current tenant/user. Consider UUIDs for resource IDs instead of sequential integers — makes blind enumeration harder (but still not a substitute for authorisation).

Broken Access Control / Privilege Escalation

  • What: regular user sends POST /admin/delete-user and it works because the admin route forgot to check role.
  • Variants: horizontal (user A → user B’s data), vertical (user → admin), context escalation (tenant A → tenant B).
  • Defence: deny by default — route-level middleware that requires explicit allowRoles(['admin']) to pass. Centralise authorisation; don’t re-implement checks in every handler.

Path Traversal (Directory Traversal)

  • What: ?file=../../../etc/passwd reads a file outside the allowed directory.
  • Defence: resolve the path with path.resolve(), then check it is under the allowed base directory. Reject .. and absolute paths. Prefer looking up files by ID in a DB and mapping to paths server-side.

Insecure File Upload

  • What: attacker uploads evil.php, server serves it as executable. Or uploads SVG with embedded JS. Or uses the upload path to overwrite another file.
  • Defence: validate file type server-side (don’t trust Content-Type header), limit size, re-encode images rather than trusting format, store uploads outside the web root, serve via a separate domain with no execution, use random filenames, scan for malware.

Cryptographic Family

Using crypto primitives the wrong way — or using primitives too weak for the threat model.

sequenceDiagram
  participant A as Attacker
  participant L as Leaked DB dump
  participant G as GPU rig
  participant T as Other sites
  L->>A: users.csv with md5(password)
  A->>G: Feed hashes + wordlist / rainbow table
  G->>G: 10B+ guesses / sec (MD5 is cheap)
  G-->>A: Majority of plaintext passwords recovered
  A->>T: Credential stuffing on banking / email / SaaS
  Note over A,T: Fast hash = offline cracking feasible<br/>bcrypt / argon2id would have made this impractical

Weak Hashing for Passwords

  • What: using MD5 or SHA-256 straight for passwords. GPUs compute billions per second.
  • Defence: use a purpose-built password hash — argon2id (first choice), bcrypt (OK), scrypt (OK). Configure cost parameters so one hash takes ~250ms on your hardware. Never invent your own.

Hardcoded Secrets

  • What: API keys, passwords, tokens in Git history. Attackers scan public GitHub for these hourly.
  • Defence: secrets from environment variables or a secret manager (AWS Secrets Manager, GCP Secret Manager, Vault). Pre-commit hooks like gitleaks or truffleHog to block accidental commits. If a secret leaks, rotate immediately — don’t just delete the commit; Git history preserves it and clones already have it.

Insecure Randomness

  • What: using Math.random() for session IDs, CSRF tokens, or password-reset tokens. Math.random is not cryptographically secure — predictable given enough output.
  • Defence: use crypto.randomBytes (Node), secrets.token_urlsafe (Python), crypto.rand (Go). When in doubt, check the docs for “crypto” / “secure random”.

Bad Crypto Choices

  • ECB mode: encrypts identical plaintext blocks to identical ciphertext blocks — the famous Tux penguin example.

  • Reused IVs / nonces: with CTR/GCM mode, reusing a nonce under the same key catastrophically breaks confidentiality and integrity.

  • No integrity: encrypting without authentication (AES-CBC without HMAC) is vulnerable to padding-oracle attacks.

  • Defence: use AES-GCM or ChaCha20-Poly1305 for authenticated encryption. Generate a fresh random nonce per message. Better still: use libsodium / crypto.subtle with AES-GCM.

Padding Oracle

  • What: when an AES-CBC system reveals via timing or error messages whether padding was valid, an attacker can decrypt arbitrary ciphertext byte-by-byte.
  • Defence: authenticated encryption (above) makes padding oracles irrelevant. If you must use CBC, add an HMAC and reject before decryption.

Server-Side Family

Making the server do something bad on the attacker’s behalf — usually by abusing the server’s network position or parsing logic.

sequenceDiagram
  participant A as Attacker
  participant S as App server
  participant Meta as Cloud metadata<br/>169.254.169.254
  A->>S: POST /fetch-image {url: "http://169.254.169.254/..."}
  S->>Meta: GET (server has private-network access)
  Meta-->>S: IAM credentials / instance role
  S-->>A: Proxies the response body
  Note over A,Meta: SSRF lets the attacker<br/>reach things only the server can see

SSRF (Server-Side Request Forgery)

  • What: your app lets users supply a URL you fetch server-side (webhooks, image URL scrapers, PDF generators). The attacker points it at http://169.254.169.254/latest/meta-data/iam/... — AWS metadata endpoint — and your server obediently fetches cloud credentials and returns them in the response.
  • Defence:
  • Block RFC1918 private ranges, link-local, loopback. Resolve DNS yourself; check IPs at both resolution and connection time (DNS rebinding).
  • Disable HTTP redirects or re-validate each hop.
  • IMDSv2 on AWS (requires a token — blocks naive SSRF).
  • Use an allow-list of hostnames if possible; if not, an explicit deny-list with DNS rebinding protection.

XXE (XML External Entity)

  • What: XML parser expands external entities. Attacker sends <!DOCTYPE ... [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> and the parser reads the file for them.
  • Defence: disable external entity loading in your XML parser (usually one flag). Most modern libraries disable it by default — check yours.

Deserialization

  • What: deserialising attacker-controlled data instantiates arbitrary classes and invokes their methods. Classic Java ObjectInputStream, Python pickle, Ruby Marshal, .NET BinaryFormatter, PHP unserialize — all have led to remote code execution.
  • Defence: never deserialise untrusted data with native formats. Use JSON/protobuf — data-only formats. If you need binary serialisation, validate with a strict schema and reject unexpected types.

RCE (Remote Code Execution)

  • What: attacker runs arbitrary code on your server. The ultimate goal of many attacks above. Common root causes: command injection, deserialization, file upload + execution, template injection, SSRF to internal admin interfaces.
  • Defence: defence-in-depth — everything above, plus running application processes with least privilege (non-root user, read-only filesystem where possible, network egress restrictions, no Docker-in-Docker without careful thought).

Open Redirect

  • What: /login?return=https://evil.com. After login, your server redirects to attacker’s page. Used for phishing — the URL starts with your domain, which victims trust.
  • Defence: validate the redirect target against an allow-list, or require it to be relative. Never echo return into a Location header without validation.

Supply Chain Family

Your code is only as safe as the weakest link in the dependency graph and build pipeline.

sequenceDiagram
  participant A as Attacker
  participant NPM as npm / PyPI
  participant D as Developer laptop
  participant CI as CI / Prod
  A->>NPM: Publish "reqeusts" (typo of "requests")
  D->>D: `npm install reqeusts` (typo)
  D->>NPM: Fetch package
  NPM-->>D: Malicious code + postinstall hook
  D->>CI: git push (lockfile now references evil pkg)
  CI->>NPM: npm ci — pulls same poisoned package
  Note over CI: postinstall exfiltrates env vars,<br/>SSH keys, cloud credentials

Malicious Packages

  • What: an npm/PyPI package you depend on is compromised or deliberately malicious. event-stream (2018), ua-parser-js (2021), left-pad (not malicious, but showed how much we depend on 11 lines of code).
  • Defence: pin versions (package-lock.json, poetry.lock), use npm audit / pip-audit in CI, review new dependencies before adding, prefer popular packages (widely reviewed), keep dependencies minimal.

Dependency Confusion

  • What: your internal package is called @company/utils. Attacker publishes a package with the same name on the public registry with a higher version number. Your build pulls the public one.
  • Defence: scoped packages, private registry with explicit precedence, or better — vendor internal packages with a unique name that isn’t claimable publicly.

Typosquatting

  • What: react-dropzonereact-drop-zone / reactdropzone. Users typo, install malicious copy.
  • Defence: careful copy-paste from the canonical package name. Lockfiles + CI scan.

Compromised Build Pipeline

  • What: attacker modifies your CI workflow, gets access to prod secrets, ships malicious code. SolarWinds is the canonical example.
  • Defence: pin GitHub Action SHAs (not @v4), require PR reviews for workflow changes, limit prod-secret exposure in CI, use OIDC for cloud access instead of long-lived credentials.

Network Family

Attacks that sit on the wire between client and server, or abuse DNS / CORS / TLS.

sequenceDiagram
  participant V as Victim laptop
  participant A as Attacker (rogue Wi-Fi AP)
  participant S as Server
  V->>A: Joins open Wi-Fi "CoffeeShop Guest"
  V->>A: HTTP GET /login (no TLS)
  A->>S: Forwards request
  S-->>A: Response
  A->>A: Reads username / password in cleartext
  A-->>V: Forwards response (victim sees nothing unusual)
  Note over V,S: TLS + HSTS preload would have stopped<br/>the plaintext exchange before it started

Man-in-the-Middle (MITM)

  • What: attacker on the network path (coffee shop Wi-Fi, rogue access point, compromised ISP) intercepts traffic.
  • Defence: TLS everywhere (HTTPS, wss://, mTLS between services). HSTS (Strict-Transport-Security: max-age=63072000; includeSubDomains; preload) so the browser never downgrades to HTTP. Pin certificates for mobile apps.

DNS Rebinding

  • What: attacker controls the DNS for their domain. First query returns their IP (serves malicious JS). Second query (from the browser’s JS, short-TTL) returns 127.0.0.1 — now the page can talk to services on your localhost.
  • Defence: check the Host header on all services (even “internal” ones); require auth even for localhost; validate that a resolved IP is public for outbound fetches.

Subdomain Takeover

  • What: your blog.company.com CNAMEs to a Heroku/Netlify/S3 bucket you no longer control. Attacker claims that bucket and now serves their content from your subdomain.
  • Defence: audit DNS regularly, remove dangling CNAMEs, use txt record tied to ownership where the platform supports it.

CORS Misconfiguration

  • What: Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers actually refuse this combo, but “reflect the Origin” + credentials is valid and lets any origin read authenticated responses.
  • Defence: allow-list origins; never reflect arbitrary Origin; only set Allow-Credentials: true where strictly needed.

Business Logic Family

This is where attackers get really creative. No specific vulnerability class — just logic your devs didn’t expect.

sequenceDiagram
  participant A as Attacker
  participant S as Server
  participant DB as DB (balance = 100)
  par Two concurrent withdraw requests
    A->>S: POST /withdraw 100
    S->>DB: SELECT balance → 100
    S->>DB: UPDATE balance = 0
  and
    A->>S: POST /withdraw 100 (same instant)
    S->>DB: SELECT balance → 100 (stale read)
    S->>DB: UPDATE balance = 0
  end
  Note over A,DB: 200 withdrawn from a 100-balance account<br/>SELECT FOR UPDATE / unique constraints would prevent this

Race Conditions (TOCTOU)

  • What: “Time of Check vs Time of Use”. You check the user has 10,thendeduct10, then deduct 10. Attacker fires 20 concurrent “withdraw $10” requests. Between the check and the update, the balance hasn’t been decremented, so all 20 pass the check.
  • Defence: database transactions with appropriate isolation, row-level locks (SELECT ... FOR UPDATE), idempotency keys on write endpoints, or atomic operations (UPDATE ... WHERE balance >= 10).

Mass Assignment / Parameter Tampering

  • What: your User.update(req.body) accepts every field in the body. Attacker adds is_admin: true to a profile update request.
  • Defence: explicit allow-list of fields on the server (pick(body, ['name', 'bio'])). Never trust client-side “hidden” fields.

Rate Limit Bypass

  • What: rate limit is per IP — attacker uses a residential proxy network. Per-account — attacker creates thousands of accounts. Per-endpoint — attacker hits a different endpoint that hits the same expensive backend.
  • Defence: rate limits at multiple granularities (IP + account + fingerprint + global), cost-aware budgets (expensive endpoints get smaller quotas), CAPTCHA on suspicious volumes, WAF layer.

Price / Quantity Manipulation

  • What: e-commerce — client sends { productId: 1, quantity: 1, price: 99.99 }. Attacker sends price: 0.01. Bad servers trust it.
  • Defence: never trust client-supplied prices. Look up the price server-side from the product ID.

OTP Reuse / OTP Bypass

  • What: 6-digit OTP, but the check doesn’t invalidate used codes — attacker replays. Or the OTP isn’t tied to the session requesting it, so OTP from user A works for user B. Or no rate limit on attempts — brute force 1M codes in minutes.
  • Defence: single-use codes (delete/mark-used on verify), bind to session/account, max 5 attempts then invalidate, short TTL.

Client-Side Family

Attacks that exploit JavaScript object semantics, the DOM, or the browser’s execution context.

sequenceDiagram
  participant A as Attacker
  participant S as Server (Node.js)
  participant P as Object.prototype
  A->>S: POST /api/settings<br/>{"__proto__": {"isAdmin": true}}
  S->>S: deepMerge(user, body)  (unsafe)
  S->>P: writes isAdmin = true on prototype
  Note over P: Every plain object in the process<br/>now inherits isAdmin = true
  A->>S: GET /admin (next request)
  S->>S: if (user.isAdmin) { … }  → truthy
  S-->>A: Admin pages served

Prototype Pollution

  • What: an object merge function takes attacker-controlled JSON. {"__proto__": {"isAdmin": true}} modifies Object.prototype so every object now has isAdmin: true.
  • Defence: don’t merge untrusted JSON into objects; use Object.create(null) for data bags; use libraries that guard (lodash’s safe merge).

DOM Clobbering

  • What: attacker injects HTML like <form id="config"><input name="debug" value="true"> — JavaScript later does if (window.config.debug), which now reads the injected DOM element.
  • Defence: prefer explicit let config = {...} over relying on global names; avoid using user-controlled IDs.

CSP Bypass

  • What: CSP configured but with unsafe-inline in script-src — no protection. Or includes a CDN that hosts JSONP / AngularJS — attacker chains through the allowed domain.
  • Defence: no unsafe-inline, no unsafe-eval, use nonces or hashes, audit every allowed source for known bypass gadgets, use 'strict-dynamic'.

Denial of Service Family

Cheap on the attacker side, expensive on the server side. You don’t need a botnet to take a service down.

sequenceDiagram
  participant A as Attacker
  participant S as Server
  A->>S: POST /validate email="aaaaaaaaaaaaaaaa!"
  S->>S: /^(a+)+$/.test(input)
  Note over S: Catastrophic backtracking —<br/>2^n paths explored, CPU pinned for seconds
  A->>S: Send 100 such requests in parallel
  Note over S: Event loop blocked → health checks fail<br/>→ load balancer evicts pod → next pod takes load
  S-->>A: (service unresponsive)

ReDoS (Regular Expression DoS)

  • What: a vulnerable regex like ^(a+)+$ takes exponential time on inputs like aaaaaaaaaaaaaaaaaaaa! — one request locks a thread for minutes.
  • Defence: avoid nested quantifiers, use linear-time regex engines (Rust’s regex, Go’s regexp), time-out regex execution, reject ludicrously-long inputs.

Zip Bomb / Billion Laughs / XML Bomb

  • What: 42 KB zip that expands to 4.5 PB. XML with nested entity references that expands exponentially.
  • Defence: size limits before decompression; expansion-ratio limits; streaming parsers with hard caps.

Slowloris / Slow Body

  • What: attacker opens thousands of connections and sends each request byte-by-byte, tying up worker threads.
  • Defence: reasonable connection/header/body timeouts, reverse proxy with slow-client protection (nginx has this by default), async I/O servers.

Resource Exhaustion (Memory / CPU)

  • What: “show me the last 1,000,000 records” — a single request allocates gigabytes. Or an image endpoint that resizes any user-supplied image with no dimension limit.
  • Defence: paginate; cap query result sizes; reject wildly-large files/dimensions; use resource limits (cgroup, Kubernetes pod limits).

Side-Channel Family

Leaking secrets not through what the system returns, but through how long it takes or how much resource it uses.

sequenceDiagram
  participant A as Attacker
  participant S as Server
  loop Guess each character in turn
    A->>S: POST /verify token="a000…"
    S->>S: memcmp(user, attacker)  (stops at first mismatch)
    S-->>A: 401 in ~12 µs (mismatch at pos 0)
    A->>S: POST /verify token="b000…"
    S-->>A: 401 in ~24 µs (match at pos 0, mismatch at pos 1)
    Note over A: Timing leak → position of first mismatch
  end
  Note over A,S: Enough samples + statistics →<br/>the whole token is recovered char by char.

Timing Attacks

  • What: string comparison that returns early on first mismatch. if (token == expected) takes longer when more bytes match — attacker guesses bytes one at a time.
  • Defence: constant-time comparison — crypto.timingSafeEqual in Node, hmac.compare_digest in Python, subtle.ConstantTimeCompare in Go.

Cache Attacks

  • What: user-specific data gets cached at a CDN and served to other users. Or the cache key doesn’t include authentication, so logged-in content leaks to anonymous visitors.
  • Defence: Cache-Control: private for personalised content, include auth context in cache keys, separate infrastructure for personalised vs. public.

Putting It Together — the OWASP Top 10

The OWASP Top 10 is a consensus list of the most impactful web vulnerability categories. Current edition covers Broken Access Control, Cryptographic Failures, Injection, Insecure Design, Security Misconfiguration, Vulnerable Components, Auth Failures, Software and Data Integrity, Logging and Monitoring Failures, SSRF. Everything in this post maps into one or more of those categories.

If you do nothing else, read the OWASP Top 10 cheatsheet series and use it as a PR review checklist.

Defence Mindset — Principles That Pay Off

Rules are fine, but principles generalise better.

  • Least privilege, everywhere. App process runs as non-root. Service accounts can read only the buckets they need. DB role can’t drop tables. Deploy key is read-only.
  • Defence in depth. Never rely on a single layer. TLS + CSP + HttpOnly + SameSite + CSRF tokens all target different gaps.
  • Fail closed. Unknown state defaults to deny. Authorization check error? Deny. Rate-limit store unreachable? Block. Token signature invalid? Reject.
  • Validate on the server. Client-side checks are UX, not security.
  • Log security-relevant events — login, password change, 2FA disable, admin actions, access-control denials. Alert on anomalies.
  • Patch regularly. Dependabot/Renovate on a weekly cadence; treat CVEs in your critical path as production incidents.
  • Assume breach. If your DB is stolen, what’s in it? Passwords must be hashed, PII ideally encrypted at rest, audit logs immutable.
  • Reduce attack surface. Fewer endpoints = fewer bugs. Kill unused features, old APIs, forgotten debug endpoints.

Tools Every Team Should Use

  • Dependency scanners: npm audit, pip-audit, Dependabot, Renovate, Snyk.
  • Secret scanners: gitleaks, truffleHog — pre-commit hooks and CI.
  • SAST (static analysis): Semgrep, CodeQL, SonarQube for rule-based code scanning.
  • DAST (dynamic): OWASP ZAP, Burp Suite Community for request-level testing.
  • Container scanners: Trivy, Grype for image CVEs.
  • IaC scanners: Checkov, tfsec for Terraform/CloudFormation.
  • Fuzzers: for mature projects — libfuzzer, afl++, jazzer.
  • Bug bounty program: when you’re ready, HackerOne / Bugcrowd — external researchers find what automated tools miss.

Wrapping Up

Security is not a single feature you add at the end. It’s a property that emerges when every developer on the team has internalised the attacker’s playbook and writes code that refuses to cooperate. The specific techniques in this post change over time — ReDoS wasn’t a common concern fifteen years ago, prompt injection didn’t exist three years ago — but the shape of attacks remains constant: data crossing a boundary, an over-trusting receiver, a primitive composed into something catastrophic.

Read this list. Think about which ones could hit your codebase. Fix the easy ones today. Build habits against the rest. When the next vulnerability class emerges, it’ll fit a pattern you already know.

Comments powered by Giscus are not yet configured. Set PUBLIC_GISCUS_REPO_ID and PUBLIC_GISCUS_CATEGORY_ID in apps/web/.env to enable.

PV

Written by Palakorn Voramongkol

Software Engineer Specialist with 20+ years of experience. Writing about architecture, performance, and building production systems.

More about me

Continue Reading