Web Application Security: OWASP Top 10, Burp Suite, and Modern Attacks
Web applications are where your users, data, and business logic meet the internet. That makes them a prime target for attackers who want to steal credentials, abuse APIs, or chain low-severity bugs into costly incidents. This guide shows you how to recognize the risks that still ship to production, and the practices that actually reduce them in real teams. You will learn where to focus, how to test efficiently, and how to design guardrails that make secure defaults the easy path for developers.
Security is not a tool you buy, it is a workflow you practice. We will connect fundamentals like the OWASP Top 10 to practical tactics, from Burp Suite workflows to SAST and DAST selection, and from token pitfalls to cloud era SSRF. Use the checklists, code examples, and step-by-step walkthroughs to upgrade how you build and break web apps. When a vulnerability class is discussed, you will see realistic examples and fixes in languages you actually use.
Why web application security still fails in production
Most web vulnerabilities are not exotic. They are features that were never meant to be features, unlocked by implicit assumptions or missing checks. Teams still ship injection, broken auth, and insecure defaults because delivery pressure is real, frameworks are complex, and it is easy to misconfigure something that looked harmless in a test. The path to fewer production issues starts with acknowledging the constraints that cause insecure code to persist, then building engineering practices to surface and block those flaws early.
Time pressure and unclear ownership create gaps. A developer reaches for a quick string concatenation to build a query, or opts for a long-lived JWT because token rotation feels risky. A reviewer skims a pull request with 500 lines and no unit tests. Ops enables a proxy or metadata endpoint for debugging and forgets to turn it off. Each choice is understandable, but together they form an attack surface that a determined tester or attacker will find and exploit. Good security work involves designing boundaries and defaults so those understandable decisions do not turn into critical exposures.
Security bugs also thrive where you lack feedback loops. If you never write tests that emulate hostile input, never run DAST in staging, and never monitor for anomalies in production, then defects survive. A robust workflow combines static checks that block high-risk patterns, dynamic tests that probe behavior, and guardrails like CSP and strict cookie attributes that make many classes of bugs difficult to exploit. The aim is not perfection, it is continuous reduction of risk at the same or lower cognitive load for teams.
Getting oriented in the broader discipline helps you pick your battles. If you are starting out, bookmark the cybersecurity learning hub to place web security in context with network, cloud, and identity. Then bring that perspective back to your backlog. You will see how underlying transport and identity goals surface in web attacks like CSRF, SSRF, and IDOR, and how cross-domain thinking helps you fix root causes, not just symptoms.
Threat modeling for web apps: assets, attackers, abuse cases
Threat modeling is how you make risk visible before code ships. Start with assets, not threats. Identify what the application protects and processes: user accounts, payment tokens, health records, internal admin capabilities, or high-value compute. Then map trust boundaries. Where does untrusted input cross into your app, your network, or your cloud provider APIs. Draw data flows between browser, CDN, app servers, databases, queues, and third-party services. This gives you a picture to annotate with misuse cases and controls.
Define attacker goals and capabilities in concrete terms. For a consumer app you might face credential stuffing, gift card fraud, or resource scraping. For an internal admin portal you must assume phishing-derived access and lateral movement attempts. For a public API you should expect misconfigured client applications leaking credentials, and also targeted exploitation of over-permissive endpoints. Model attackers with the same creativity you apply to product personas, but grounded in technical constraints and logs of real incidents in your ecosystem.
Turn architecture and attacker insights into abuse cases. Ask, given this flow, what happens if this parameter is missing, excessively long, or contains serialized state. What if a client can replay this request, or swap a resource identifier from their own ID to someone else. What if the server fetches a URL provided by a user and the URL points to a cloud metadata service. What if a worker processes an uploaded file and the file is a ZIP bomb. These questions translate into specific tests, safe defaults, and backlog items, such as adding server-side object permission checks, or isolating file parsing in a sandbox.
Threat modeling is most effective when embedded in planning, not bolted on. Add a lightweight template to your user stories. For each new endpoint, list input sources, required authentication, expected authorization checks, idempotency requirements, and data validation rules. Track mitigations like parameterized queries, strict schema validation, and replay protection. Make threat modeling a team habit, not a quarterly meeting that creates shelfware. The payoff is fewer surprises, faster code reviews, and clearer acceptance criteria for security.
The OWASP Top 10 today: what changed and what still bites
The OWASP Top 10 is a curated view of the most impactful and common web application risks. It evolves as data and exploitation trends change, but many root causes recur. Injection remains a theme, but so do design flaws in authentication and authorization, cryptographic failures, insecure design, misconfigurations, and insufficient logging. Treat the list as a compass, not a compliance checklist. It points you to classes of issues you must design out, test for, and monitor in production.
Here is a pragmatic view of common OWASP categories with examples you are likely to encounter and fix:
| Category | Example in practice | Practical first-line defense |
|---|---|---|
| Broken Access Control | IDOR on order history, missing admin role check on export | Enforce server-side object checks, centralized authorization policies |
| Cryptographic Failures | Insecure JWT signing, weak password hashing | Strong algorithms and libs, key rotation, salted adaptive hashing |
| Injection | SQLi, NoSQLi, OS command injection | Parameterized queries, avoid shell calls, strict input parsing |
| Insecure Design | Missing rate limits, no workflow-level checks | Abuse case-driven design, business logic validation |
| Security Misconfiguration | Debug endpoints, permissive CORS | Hardened defaults, infrastructure as code reviews |
| Vulnerable and Outdated Components | Old ORM with known SQLi bug | Dependency scanning and timely patching |
| Identification and Authentication Failures | Brute-forceable login, password reset flaws | MFA, credential stuffing defenses, secure reset workflows |
| Software and Data Integrity Failures | Unpinned dependencies, compromised update channels | Integrity checks, signed artifacts, supply chain controls |
| Security Logging and Monitoring Failures | No audit trail for critical actions | Centralized logs, alerting for anomalies |
| Server-Side Request Forgery | Backend fetches to internal hosts | Network egress controls, allowlists, URL parsing hardening |
Use official OWASP resources for deeper context and testing guidance. The OWASP Top Ten project is a reliable reference for detailed descriptions, risks, and examples. Map your application to these categories. For each, identify existing mitigations, planned improvements, and gaps in testing coverage. Integrate relevant checks into code review templates and CI to keep the list actionable.
A meaningful way to adopt the Top 10 is to attach each category to specific engineering standards. For example, define the ORM patterns that block SQLi in your stack, the middleware that enforces object permission checks, and the cookie configuration that prevents session theft. Then write tests that fail if a route lacks the required middleware or if a DTO allows arbitrary additional properties. Treat categories as triggers to define your paved road, not as labels that live in a slide deck.
Injection flaws across stacks
Injection is what happens when untrusted input becomes part of an interpreter command, such as a SQL statement, a shell command, or a template expression. The fix pattern is consistent: do not compose commands with raw input, and pass untrusted data through safe parameterization or strict parsers. Many frameworks make this easy if you follow their APIs, but it is still common to find string concatenation, dynamic field names, or ad hoc shell calls that bypass safety rails. Knowing the variants across your stack helps you recognize and fix them aggressively.
SQL injection is well known, but it keeps appearing in legacy code, custom reporting tools, and administrative endpoints. Parameterized queries are the baseline. Do not build SQL by concatenating strings. Avoid dynamic ORDER BY or column names that are directly derived from user input, or at least map user input to a whitelist of allowed identifiers. Also validate device or tenant IDs used in queries so you do not introduce logical flaws while fixing injection.
In NoSQL databases, injection looks different. For example, in MongoDB or DocumentDB you might accidentally allow an attacker to inject query operators by passing JSON directly into a query. Treat client parameters as strings or numbers and transform them into typed filters, not arbitrary subtrees. Employ schema validation and object sanitization to strip unexpected keys like $ne or $gt. Remember that flexible query languages often accept patterns and regex, which can be abused to cause performance issues.
Operating system command injection occurs when you pass untrusted input to shell commands. Avoid shelling out when a library call exists. If you must execute a command, use safe APIs that pass arguments as an array without invoking a shell. Validate and restrict allowed values to a small set. Prefer configuration-driven execution rather than open-ended command formatting.
Example: SQL injection in Node.js with fixes
Vulnerable:
// Express route that builds SQL dangerously
app.get('/users', async (req, res) => {
const sort = req.query.sort || 'name';
const search = req.query.q || '';
const sql = `SELECT id, name FROM users WHERE name LIKE '%${search}%' ORDER BY ${sort}`;
const rows = await db.query(sql);
res.json(rows);
});
Fixed with parameterization and safe identifier mapping:
// Safe columns only
const allowedSort = { name: 'name', created: 'created_at' };
app.get('/users', async (req, res) => {
const sort = allowedSort[req.query.sort] || 'name';
const search = req.query.q || '';
const rows = await db.query(
'SELECT id, name FROM users WHERE name LIKE ? ORDER BY ' + sort,
[`%${search}%`]
);
res.json(rows);
});
Example: NoSQL injection in a MongoDB filter
Vulnerable:
// Directly trusts JSON body for a filter
app.post('/find', async (req, res) => {
const filter = req.body.filter; // Attacker can inject {$ne: null} etc.
const docs = await users.find(filter).toArray();
res.json(docs);
});
Fixed with schema validation and narrowing to typed filters:
// Only allow simple field equality and safe regex
function buildFilter(input) {
const out = {};
if (typeof input.email === 'string') out.email = input.email;
if (typeof input.name === 'string') out.name = new RegExp('^' + escapeRegex(input.name));
if (typeof input.active === 'boolean') out.active = input.active;
return out;
}
app.post('/find', async (req, res) => {
const filter = buildFilter(req.body || {});
const docs = await users.find(filter, { projection: { _id: 0, email: 1, name: 1 } }).toArray();
res.json(docs);
});
Example: Command injection in Python
Vulnerable:
import os
def convert(input_path, output_path, format):
os.system(f"convert {input_path} -quality 80 {output_path}.{format}")
Fixed:
import subprocess
from pathlib import Path
def convert(input_path, output_path, format):
allowed = {'png', 'jpg', 'webp'}
fmt = format.lower()
if fmt not in allowed:
raise ValueError("Unsupported format")
inp = Path(input_path)
out = Path(output_path).with_suffix('.' + fmt)
subprocess.run(['convert', str(inp), '-quality', '80', str(out)], check=True)
Authentication, authorization, and IDOR
Most catastrophic data exposures are not clever XSS chains, they are missing or flawed authorization checks. Identification and authentication decide who a user is. Authorization decides what that user can do. In many codebases, authN and authZ are split across middleware, controllers, and services, which makes it easy to miss a check, or to trust a client-supplied user_id parameter. To harden this, make authorization explicit, centralized, and testable, and ensure you cover object-level access, not just role-based route access.
IDOR, or insecure direct object reference, happens when an attacker changes a resource identifier in a request to one they should not access. If your route fetches a profile by ID from a query string, and does not verify ownership or permission, then a user can enumerate IDs and pull other profiles. Role checks often do not prevent IDOR, because both users might share the same role. The correct solution is to validate that the subject is allowed to access the specific object instance based on ownership or policy.
A robust approach is to pass the authenticated subject identity to your data layer and have object-level policies enforced alongside the query. For example, profileService.getProfile(subject, profileId) should either fetch and check ownership or be implemented as a join that only returns owned records. Avoid patterns that fetch an object by ID and then check if subject.id equals object.ownerId in the controller, because it is easy to forget that check on another route. Centralize policy evaluation and make it impossible to fetch unfiltered objects.
Do not trust any client-supplied identity hints. Never accept a user_id or role in a request body as authoritative. Derive the subject from the session or token. If you support multi-tenant data, make tenant constraints part of every query by design, not by convention. Use defense in depth by limiting data fields returned for non-admins. For actions like password reset, enforce strong verification and single-use tokens.
Example: IDOR in a profile endpoint with fix
Vulnerable:
// Route trusts user_id from query string
app.get('/profile', async (req, res) => {
const profile = await Profiles.findById(req.query.user_id);
res.json(profile);
});
Fixed with subject-derived access and data-layer enforcement:
// subject comes from session middleware
app.get('/profile', async (req, res) => {
const subjectId = req.session.userId;
const profile = await Profiles.findOwnedProfile(subjectId);
res.json(profile);
});
// In Profiles data access layer
async function findOwnedProfile(subjectId) {
return db.query('SELECT id, name, email FROM profiles WHERE owner_id = ?', [subjectId]);
}
Checklist for robust authorization
- Derive subject from authenticated context only, never from request parameters.
- Enforce object-level policies where data is fetched, not after.
- Use deny-by-default for all endpoints, and allow explicitly based on policy.
- Write unit tests and integration tests that attempt to access another user’s object IDs.
- Centralize authorization logic in a library or middleware that applies consistently.
- Consider how zero trust principles apply to your internal admin tools and service-to-service calls.
Sessions, cookies, and JWT pitfalls
Sessions and tokens are how your app remembers who a user is across requests. Cookies provide transport for session identifiers and can enforce strong browser-level defenses when configured correctly. JWTs are compact tokens that carry claims and a signature, commonly used in APIs and single-page applications. Both models are viable, but misconfigurations produce real risk. Choose one based on your architecture, then follow strict operational practices for key management, rotation, revocation, and storage.
Cookie-backed server sessions are simple and powerful. A server generates a random session ID, stores user state server-side, and sends the ID to the client in an HttpOnly, Secure, SameSite cookie. HttpOnly blocks JavaScript from reading the cookie. Secure restricts it to HTTPS. SameSite reduces cross-site request forgery by not sending the cookie on cross-site requests unless explicitly set. Rotate session IDs at login, and on permission changes. Invalidate sessions server-side when users log out or reset passwords. Regenerate cookies when you suspect theft.
JWTs introduce common pitfalls. Some libraries used to accept the none algorithm, which means unsigned tokens. Others mix symmetric and asymmetric algorithms incorrectly, allowing algorithm confusion attacks where a public key is used as an HMAC secret. Long-lived JWTs become bearer secrets that are hard to revoke if leaked. Storing JWTs in localStorage invites theft through XSS. Many teams try to reinvent revocation lists and token introspection. If you use JWTs, prefer short-lived access tokens, store them in HttpOnly cookies, and use refresh tokens tied to server-side session state. Avoid placing sensitive data in JWT claims.
Implement token rotation and revocation carefully. For JWTs, use an authorization server that issues short-lived access tokens and long-lived refresh tokens with rotation on use. Keep a server-side store of refresh token identifiers to revoke on suspicion. For cookie sessions, store metadata such as user agent and IP hash to detect anomalies. For both models, bind important actions to additional verification like re-authentication or step-up MFA. Never rely on client-side checks for sensitive flows.
Refer to the standard for JWT to avoid homegrown assumptions. The IETF specification defines the structure, algorithms, and claims model. Reading it once helps you avoid common mistakes and understand what your libraries are doing. You can find the standard here: IETF RFC 7519 on JSON Web Token. Translate this into engineering norms: only allow vetted algorithms, enforce audience and issuer checks, and do not overload JWTs with mutable state.
Cookie configuration snippet
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
Common JWT missteps to avoid
- Accepting tokens with alg set to none or unexpected algorithms.
- Using a public RSA key as an HMAC secret due to algorithm confusion.
- Creating tokens that live for hours or days without server-side revocation.
- Storing tokens in localStorage or sessionStorage instead of HttpOnly cookies.
- Embedding sensitive data in claims that can be decoded by anyone.
Cross-site attacks: XSS, CSRF, and clickjacking
Cross-site attacks exploit the browser’s trust decisions. XSS allows an attacker to execute script in your origin, often to steal sessions, pivot into sensitive actions, or alter the UI. CSRF causes a victim’s browser to send authenticated state-changing requests to your application without their intent. Clickjacking overlays your site in a hidden frame to trick users into clicking on actions. Preventing these requires output encoding, strict cookie and token policies, and response headers that make attacks difficult or impossible.
XSS has three main forms. Reflected XSS injects payloads into immediate responses based on query parameters. Stored XSS persists payloads in the database and serves them to other users. DOM-based XSS results from client-side JavaScript reading and writing the DOM unsafely. Use context-aware output encoding for HTML, attributes, URLs, and JavaScript. Use a templating engine that escapes by default for HTML contexts. Sanitize rich text using well-reviewed libraries. Implement a Content Security Policy that limits script sources and blocks inline execution where feasible.
CSRF prevention is straightforward if you handle cookies and tokens correctly. For state-changing endpoints that rely on cookies for authentication, require a CSRF token in a header or in the request body, and verify it server-side. Set SameSite on cookies to Lax or Strict where it does not break SSO flows. For APIs used by SPAs from your own origin, store tokens in HttpOnly cookies and use a csrf-token header pattern. Do not use CORS alone for CSRF defense, since many browsers will still send cookies on simple cross-site requests if SameSite allows it.
Clickjacking defenses are set at the response header level. Use X-Frame-Options DENY or SAMEORIGIN to block framing where not needed. For modern browsers, also set a Content Security Policy frame-ancestors directive to limit which sites may embed yours. For legitimate embedding, use defensive UI patterns like requiring explicit user actions, or using frame busting only as a last resort.
Example: Escaping and CSP
Vulnerable React component:
// Dangerous use of dangerouslySetInnerHTML with untrusted content
<div dangerouslySetInnerHTML={{ __html: props.commentHtml }} />
Safer approach:
// Convert to plain text or sanitize server-side
<p>{props.commentText}</p>
Set a CSP header to limit script sources:
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; object-src 'none'; frame-ancestors 'none'
CSRF token pattern
- Generate a random CSRF token and tie it to the user session.
- Render the token into initial HTML as a meta tag, or fetch it on login via JSON.
- On each state-changing request, include it in a header like X-CSRF-Token.
- Verify it server-side and rotate on login or session renewal.
SSRF and server-side fetches in the cloud
Server-side request forgery happens when your server fetches a URL provided by an attacker. If your backend is allowed to reach internal services, metadata endpoints, or sensitive networks, an attacker can use your server as a proxy. In a cloud environment, SSRF is especially dangerous because EC2 or similar instances often expose metadata services that return credentials and configuration. Restricting egress and hardening URL parsing are critical. If your app must fetch URLs, treat them as hazardous input with strict policy.
The first control for SSRF is network-level egress filtering. Your app container or instance should not be able to reach arbitrary internal hosts by default. In cloud platforms, isolate instances so they cannot reach metadata without explicit need, and use IMDSv2 in AWS which requires session-oriented requests that are harder to abuse from simple SSRF gadgets. Review your network routes, NAT gateways, and service mesh policies to be sure only expected destinations are reachable. Consider private allowlists of external domains you fetch, validated against DNS and IP after resolution.
The second control is URL parsing and validation. Naive checks like startsWith("http://") are not enough. Parse URLs using robust libraries, block schemes like file, gopher, and data, and resolve DNS then re-check that the final IP is not in private or link-local ranges. Beware of DNS rebinding and aliases. Follow redirects only if the final destination passes policy. Do not leak fetch errors or timing differences that could become a blind SSRF oracle. If you accept webhooks or user-provided endpoints, verify ownership through out-of-band validation.
Targeted defense-in-depth measures add resilience. Add timeouts, size limits, and content type checks. Use a separate service or worker for fetching untrusted URLs with a constrained network profile. Scrub or proxy downloads through an antivirus or sandbox if you process or display fetched content. Keep metadata endpoints locked down. In AWS, for example, enforce IMDSv2 and disable IMDS if not needed. You can review official guidance on configuring the instance metadata service here: AWS EC2 IMDS configuration.
Example: URL fetcher with SSRF checks in Node.js
import { URL } from 'url';
import dns from 'dns/promises';
import net from 'net';
import fetch from 'node-fetch';
function isPrivateIp(ip) {
const blocks = [
['10.0.0.0', 8],
['172.16.0.0', 12],
['192.168.0.0', 16],
['127.0.0.0', 8],
['169.254.0.0', 16],
['::1', 128],
['fc00::', 7],
['fe80::', 10]
];
// Implement CIDR check or import a library
// Placeholder: reject RFC1918 and link-local with a library in real code
return false;
}
async function safeFetch(userUrl) {
const u = new URL(userUrl);
if (!['http:', 'https:'].includes(u.protocol)) throw new Error('Invalid scheme');
const addrs = await dns.lookup(u.hostname, { all: true });
for (const a of addrs) {
if (isPrivateIp(a.address)) throw new Error('Blocked address');
}
const res = await fetch(u.toString(), { size: 1_000_000, timeout: 3000, redirect: 'manual' });
return res;
}
Dangerous parsers, deserialization, and file handling
Many web apps accept files or serialized data. Untrusted data passed to unsafe parsers can lead to code execution or file system access. In languages with native serialization like Java or Python, deserialization of untrusted objects can invoke gadget chains that execute methods unexpectedly. Templating engines can interpolate user input into logic. Upload handlers can allow path traversal or poison content-type detection. Reducing risk requires choosing safe formats, using whitelists, and isolating parsing from the rest of your process.
Insecure deserialization happens when you decode an object graph from untrusted input using a general-purpose serializer. In Java that could be ObjectInputStream, in Python pickle, in PHP unserialize. The safest path is not to deserialize untrusted data at all. Use formats like JSON with strict schema validation. If you must use a serializer, use a library that supports type whitelisting. In event-driven systems, sign and verify messages instead of trusting type information found on the wire.
Server-side template injection, SSTI, occurs when user input flows into a server template context and gets evaluated. This might happen in Jinja2, Twig, or other engines if unescaped input is treated as a template expression. Switch to auto-escaping modes, pass user data only as data, not as templates, and apply strict filters when you have to include HTML. For templated emails or documents, treat templates as trusted code and store them under change control, not in user-editable fields.
File upload handling is a frequent source of problems. A user may upload a file with a crafted name containing ../ to escape directories, or with a double extension like image.jpg.php if your server misconfigures handlers. Never use the original filename when saving. Generate a new random name and store outside the web root. Enforce content type by inspecting magic bytes, not just by trusting the Content-Type header. Set size limits and image processing timeouts to avoid resource exhaustion. Scan uploads with antivirus where appropriate and perform processing in a sandbox.
Example: Python pickle deserialization flaw with fix
Vulnerable:
import pickle
def load_state(data):
# Attacker can craft a pickle that executes code on load
return pickle.loads(data)
Fixed:
import jsonschema, json
STATE_SCHEMA = {
"type": "object",
"properties": {"name": {"type": "string"}, "count": {"type": "integer", "minimum": 0}},
"required": ["name", "count"],
"additionalProperties": False
}
def load_state(data):
obj = json.loads(data)
jsonschema.validate(obj, STATE_SCHEMA)
return obj
Example: Safer file upload in Node.js
import crypto from 'crypto';
import fs from 'fs/promises';
import { fileTypeFromBuffer } from 'file-type';
async function saveUpload(buffer) {
const type = await fileTypeFromBuffer(buffer);
const allowed = new Set(['image/png', 'image/jpeg', 'image/webp']);
if (!type || !allowed.has(type.mime)) throw new Error('Unsupported type');
const name = crypto.randomBytes(16).toString('hex') + '.' + type.ext;
const path = '/srv/app/uploads/' + name; // outside web root
await fs.writeFile(path, buffer, { flag: 'wx' });
return name;
}
Race conditions and concurrency bugs in web stacks
Race conditions arise when two or more requests manipulate the same state concurrently and your code assumes a sequence that is not enforced. In web apps, this often shows up as double spending a balance, duplicate coupon redemption, creating the same record twice, or bypassing rate limits. Attackers can exploit races by issuing concurrent requests or by replaying requests quickly. Good engineering includes idempotency, transactional integrity, and unique constraints so that races become harmless.
Idempotency is a property of an operation that makes repeated calls safe. For POST requests that create resources or perform payments, use idempotency keys that ensure the same request applied twice has the same effect only once. Store the result indexed by the key. For updates like inventory deduction, use database-level checks such as UPDATE ... WHERE quantity >= x and verify affected rows. Avoid read-modify-write sequences without a transaction. If you must perform sequential steps, isolate them in a transaction or enforce a locking mechanism.
Database constraints are your friend. Even with careful code, concurrent execution can surprise you. Unique indexes, foreign keys, and check constraints enforce safety at the storage layer. Combined with transactions, they prevent duplicate creations and maintain invariant properties. When an operation fails due to a constraint, handle it gracefully and return a meaningful error. Prefer this to custom locking logic where possible, since your database is already optimized to do this work.
In distributed systems, you cannot rely on strict ordering. Embrace eventual consistency designs that avoid brittle dependencies on timing. For example, when processing webhook events, use a deduplication table keyed by event ID. For rate limits or counters, choose atomic operations like INCR with a TTL in Redis. For workflows that must not overlap, such as generating a one-time token, use unique tokens tied to a single-use record with a unique index on the token column. Test with tools that send concurrent requests and assert on invariants.
Example: Preventing double purchase with idempotency
Vulnerable:
// Creates an order and charges card on each POST
app.post('/checkout', async (req, res) => {
const order = await Orders.create({ userId: req.user.id, items: req.body.items });
const charge = await Payments.charge(req.body.card, order.total);
res.json({ orderId: order.id, chargeId: charge.id });
});
Fixed with idempotency key:
app.post('/checkout', async (req, res) => {
const key = req.headers['idempotency-key'];
if (!key) return res.status(400).json({ error: 'Missing idempotency key' });
const existing = await Idempotency.findByKey(key);
if (existing) return res.json(existing.response);
const order = await Orders.createUnique(req.user.id, req.body.items, key); // uses unique index on (user_id, key)
const charge = await Payments.charge(req.body.card, order.total);
const response = { orderId: order.id, chargeId: charge.id };
await Idempotency.save(key, response);
res.json(response);
});
API security fundamentals for REST and GraphQL
APIs are not just thin wrappers over databases. They encode business logic, permission checks, and rate policies. A secure API enforces authentication and authorization on every call, validates inputs against a strict schema, limits resource usage, and returns errors that do not leak internals. Whether you use REST or GraphQL, secure-by-default practices are similar: least privilege, explicit schemas, and protective controls at the edge and service layers.
Authentication in APIs should be explicit and documented. Use OAuth 2.0 or a similar protocol for third-party access. For first-party clients, prefer short-lived tokens over static API keys. Enforce TLS everywhere. Consider binding tokens to client instances where possible. For each endpoint or resolver, enforce authorization based on the subject and the specific object or field being accessed. Do not trust claims like is_admin sent by clients. Extract identity from a verified token or session and evaluate policies server-side.
Apply strict input validation and schema enforcement. In REST, use OpenAPI or JSON Schema to define payloads, then generate server validators that reject extra fields and incorrect types. In GraphQL, leverage the type system, but also enforce query complexity, depth limits, and disable or restrict introspection in production if you do not need it. Consider a safelist of operations in high-risk contexts. For both styles, normalize and canonicalize input before checks, and reject on first validation error.
Control resource usage to prevent abuse. Use rate limits per user and IP. Implement pagination and hard caps on page size. In GraphQL, set limits on query depth, field count, and cost. For file downloads or large computations, require jobs and callbacks rather than long-running synchronous calls. For potentially dangerous actions like server-side URL fetches, apply the SSRF protections described earlier. Finally, log and monitor API usage for anomalies, and document error responses so clients can handle rate limit and auth failures correctly.
API security lives within an identity and network context. Many teams find it useful to frame APIs within broader access patterns such as device posture and service-to-service auth. Explore the fundamentals in zero trust principles to align how your APIs authenticate and authorize with the rest of your environment. If your APIs live in a public cloud, align edge security, secrets storage, and network egress controls with the guidance in cloud security fundamentals.
SAST, DAST, IAST, and SCA: choosing and combining
Security testing tools find different classes of issues. Each technique has strengths and blind spots. Combine them to get broad and deep coverage without slowing teams to a halt. Static analysis inspects code without running it, dynamic analysis probes running apps, interactive tools instrument apps during test runs, and software composition analysis focuses on dependencies. Selecting and tuning these tools for your stack and workflow is more important than buying them all.
Here is a comparison to guide adoption:
| Technique | What it finds well | What it misses or struggles with | Where to run | Developer experience |
|---|---|---|---|---|
| SAST | Injection patterns, dangerous APIs, hardcoded secrets | Runtime-only bugs, authorization logic, context-heavy paths | Pre-commit, CI on PRs | Immediate feedback, can be noisy without tuning |
| DAST | XSS, CSRF, misconfig, some auth/session issues | Code-level flaws, business logic, non-HTTP flows | Staging with a seeded user | Black-box, valuable but slower feedback |
| IAST | Contextual injection, unsafe sinks, framework misuses | Non-instrumented paths, complex distributed flows | During integration tests | Actionable traces, lower noise if tests are good |
| SCA | Known vulnerable libraries, licenses | Zero-day in deps, custom code bugs | CI and at artifact build | Clear actions, low friction |
Integrate SAST early for quick wins. Start with rules that block the highest-risk patterns such as SQL string concatenation, dangerous deserializers, or shell execution APIs. Suppress or fix false positives with annotations to maintain trust. Keep the policy small and high signal, then expand. Run SAST as a pre-commit hook for fast feedback and as a required check in CI. Pair with secret scanning to prevent credential leaks.
Use DAST where it shines. Stand up a staging environment with production-like config and seed it with test data and users. Point a DAST scanner that understands authenticated workflows at it. Exclude non-critical paths to focus on the core app. Use DAST to validate session settings, error handling, CORS, CSP, and to catch reflected XSS or open redirects. Do not expect DAST to find deep business logic bugs. Use it as a guardrail and a regression safety net.
IAST adds high-context findings during real tests. Instrument your app in test runs so that taint tracking can detect injection through specific sinks. This raises the signal on issues SAST might flag generically. Combine IAST results with your test suite to grow coverage where it matters. Finally, SCA should run in every build to keep dependencies current. Tune policies to your risk tolerance and plan time to upgrade major versions. When a critical CVE drops, SCA should give you immediate impact visibility.
Using Burp Suite effectively
Burp Suite is the workbench of many web testers. It sits between your browser and the target, capturing and modifying traffic. Used well, it accelerates recon, manual testing, and exploitation in a controlled way. Whether you use the Community or Professional edition, knowing how to chain its features matters more than license level. Structure your workflow so that you answer questions quickly: What is the app doing, how does it handle edge cases, and where do checks live.
Start with Proxy and Target. Configure your browser to use Burp’s proxy and install its CA certificate to intercept HTTPS. Browse the application to build a site map in the Target tab. Turn on passive scanning to collect interesting observations like missing security headers. Use the Scope feature to restrict scanning and avoid noise. Right-click items to send them to Repeater for controlled replay and mutation.
Repeater is your best friend for understanding behavior. Take a login request, change one parameter at a time, and observe responses. Toggle cookies, headers, and methods to see how the app validates sessions and CSRF tokens. Use tabs to keep variants visible. For IDOR, capture a request to a resource, swap the ID with another user’s, and repeat. For SSRF, try internal IPs and observe error timing. Combine Repeater with Comparer to diff responses for subtle changes.
Use Intruder for systematic fuzzing when you need to spray values. Choose a position such as a numeric ID or a header, and load a payload list. Calibrate throttling and thread count to avoid crashing the target. Use Grep - Match to highlight interesting patterns in responses, like admin, error, or stack trace. Intruder is effective for enumerating usernames, testing for rate limit gaps, or finding weak tokens. For JWT analysis, intercept and modify tokens to test signature verification, audience checks, and algorithm handling.
Extend with BApp Store add-ons to go beyond basics. Active Scan, Collaborator, and Logger++ add power. Collaborator helps detect blind SSRF and out-of-band interactions. Decoder and Sequencer are useful to analyze randomness and to inspect encodings. Record findings as you go, and debrief your team with concrete reproduction steps and risk descriptions. To improve hands-on technique across the kill chain, pair this with the workflow in our hands-on penetration testing guide.
Secure SDLC and DevSecOps integration
Secure software delivery is an engineering practice, not a security department. Embed security tasks in each phase of delivery. In planning, add abuse cases and acceptance criteria. In development, give developers paved road libraries and linters. In testing, run SAST, SCA, and DAST where they add signal. In deployment, protect secrets and lock down configs. In operations, monitor anomalies and prepare playbooks for incident response. The goal is to reduce friction while steadily increasing assurance.
In planning, make threat modeling part of backlog grooming. For each story that introduces a new endpoint, define its authentication, authorization, and data validation plan. For each story that handles files, define allowed types, size limits, and processing sandboxing. Add rate limit and audit requirements. Define how this change affects logging and metrics. This improves developer clarity and reduces late surprises in review.
In development, provide secure defaults. Ship an app template with hardened headers, CSP, and cookie settings. Offer libraries for authorization that enforce object-level checks. Add code generation that stamps parameterized queries and schema validation into data access. Run SAST and secret scanning locally and in CI. Require peer reviews that assess auth and data flows and not just style. Document patterns with examples and tests.
In testing and deployment, automate checks. Gate merges on SAST high findings and SCA criticals that have fixes. Run IAST during integration tests. Run DAST nightly against staging. Keep your infrastructure as code under review to avoid misconfigurations that undo app-level controls. Rotate keys and tokens on a schedule, and maintain backup and incident response plans that include web services. The more your apps rely on cloud-managed services, the more your SDLC must coordinate with cloud identity and network controls. Deepen that connection with the practices in cloud security fundamentals and related network security basics.
Input validation and output encoding, in practice
Input validation and output encoding are two sides of the same coin. Validate to ensure data is what you expect before it touches business logic or storage. Encode to ensure that output renders safely in its destination context. Many teams do one without the other and end up with either broken features or lingering XSS and injection bugs. The key is to choose the right tool and apply it consistently where data crosses boundaries.
Validate on the server with schemas. For JSON, adopt JSON Schema or a typed validator in your language, and reject any request that includes fields not defined in the schema or values of the wrong type or length. Use canonicalization to strip or normalize inconsistent encodings before comparing or hashing. Enforce business rules, such as password length and complexity, at validation time and return clear error messages. Do not attempt to rely solely on client-side validation, since it is trivial to bypass.
Encode output based on context. HTML requires HTML entity encoding, JavaScript requires JavaScript string encoding, URLs require percent encoding, and CSS requires CSS encoding. If you use a templating system, pick one that applies the right encoding by default for your primary context, and be explicit when switching contexts. Avoid building HTML attributes with untrusted values without proper escaping. When in doubt, treat all user-controllable data as unsafe until encoded.
Watch out for Unicode and normalization edge cases. Homoglyphs and different canonical forms can cause a value to pass one check and fail another, or to be treated differently by the database and the application. Apply a portable normalization form like NFC to inputs before validation. When comparing emails or domain names, lowercase and strip trailing dots safely. When processing file paths, resolve canonical paths before checking for traversal. These details are the difference between a bug and a hardened boundary.
Example: Express validator with strict schema
import { z } from 'zod';
const CreateUser = z.object({
email: z.string().email().max(254),
name: z.string().min(1).max(100),
password: z.string().min(12).max(128)
}).strict(); // no unknown keys
app.post('/users', async (req, res) => {
const parsed = CreateUser.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ errors: parsed.error.issues });
const { email, name, password } = parsed.data;
// Proceed with password hashing and user creation
});
Cryptography basics for web apps, without footguns
Cryptography in web apps mostly means transport security, password hashing, token signing, and data encryption at rest. You do not need to design new cryptographic protocols, but you do need to avoid unsafe primitives and poor key handling. The safe path is to use vetted libraries, follow modern defaults, and automate key rotation. Mistakes in this area are often subtle and long-lived, so invest in clear standards for your stack.
For passwords, never encrypt. Hash with a slow, adaptive algorithm and a per-user salt. bcrypt, scrypt, and Argon2 are standard choices. Tune cost factors to be slow enough to deter offline attacks without harming UX, for example targeting 100 to 250 ms per hash on your hardware. Rehash with higher cost factors when users log in if you detect an outdated hash. Store password reset tokens as hashes too, not plaintext.
For tokens and signatures, stick to standard JWT libraries and strong algorithms. Avoid rolling your own HMAC or signature code. Enforce a single algorithm family in verification code to prevent confusion. Keep keys in a secure store, rotate them, and publish key identifiers and JWKS endpoints if you run your own auth. For CSRF tokens and nonces, use cryptographically strong random values from secure generators, not Math.random.
For encryption at rest, prefer managed services that handle keys and rotation for you. When you must encrypt data in your app, use high-level APIs that provide authenticated encryption, like AES-GCM, and include associated data if needed. Never reuse nonces. Derive keys using a KDF like HKDF if starting from shared secrets. Encrypt small fields like SSNs or card tokens at the edge and isolate usage. Avoid mixing encryption and compression without careful analysis, to sidestep side-channel risks.
Example: Password hashing in Node.js with Argon2
import argon2 from 'argon2';
// Hash
const hash = await argon2.hash(password, {
type: argon2.argon2id,
timeCost: 3,
memoryCost: 64 * 1024,
parallelism: 1
});
// Verify
const ok = await argon2.verify(hash, inputPassword);
Logging, error handling, and safe observability
You cannot fix what you cannot see. Logging and observability are how you confirm that controls work and how you detect unusual behavior. But logs can become liabilities if they leak sensitive data or create their own side channels. Balance visibility and privacy by adopting structured logging, redaction, and clear severity levels. Then tie logs to alerts and response playbooks that focus on real threats like brute force, token abuse, and SSRF attempts.
Use structured, context-rich logs. Include request IDs, user IDs, tenant IDs, and key metadata. Log security decisions, such as allow or deny outcomes for authorization, and reasons for denials. Avoid logging secrets, tokens, or full payloads. Redact fields like passwords and card numbers at the serializer level, not by convention. Implement sampling for high-volume endpoints to control costs while preserving signal.
Handle errors safely. Return generic messages to clients with error codes for support. Avoid stack traces in production responses. In server logs, capture full stack traces and metadata for debugging, then aggregate in a centralized system. For common attacks, produce specific signals. For example, detect and log CSRF token mismatches, repeated login failures from the same IP and user agent, unexpected cookie attributes, and SSRF attempts with private IPs.
Tie logs to detection and response. Build alerts for anomalies like sudden spikes in 401s or 403s, repeated IDOR attempts, or unexpected origins in CORS preflights. Practice incident response drills that include web app vectors. Document how to rotate keys, invalidate sessions, and apply emergency configuration changes like stricter CSP. Align this with the broader security posture you maintain across infrastructure and identity. For a primer on the broader landscape, see network security basics and cloud security fundamentals.
Secure configuration and headers that do real work
Setting the right headers can neutralize entire classes of bugs or blunt their impact. This is low effort and high return when applied correctly. Use a standard set that configures browser behavior in your favor. Combine headers with application logic to enforce defense in depth. Test them in staging with DAST and browser developer tools to confirm they do what you expect.
Key headers to deploy include:
- Strict-Transport-Security: tells browsers to prefer HTTPS for your origin.
- Content-Security-Policy: limits sources for scripts, frames, images, and more.
- X-Frame-Options or CSP frame-ancestors: prevents clickjacking by blocking framing.
- X-Content-Type-Options: nosniff prevents MIME type confusion.
- Referrer-Policy: limits how much referral information is sent.
- Permissions-Policy: controls APIs like geolocation and camera.
A typical secure baseline looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer
Permissions-Policy: geolocation=(), camera=(), microphone=()
Tune CSP realistically. A strict CSP that you cannot maintain becomes a liability. Start with script-src 'self' and a small set of CDNs you truly need. Avoid unsafe-inline where possible by using nonces. For frameworks that inject runtime code, use strict-dynamic carefully and test with your DAST workflow. Monitor CSP violations with a report-uri endpoint to see where your pages attempt to load disallowed resources.
Testing JWT, CSRF, IDOR, SSRF, and XSS with step-by-step walkthroughs
Hands-on testing makes these concepts concrete. Here is a set of repeatable steps you can apply with Burp Suite and a browser to validate common web security controls. Treat these as scripts you can adapt to your own application. Save your observations and share them with your team to inform fixes and guardrails.
To test JWT handling:
- Intercept a request with an Authorization: Bearer header.
- Decode the JWT in Burp Decoder. Verify header.alg and claims like iss, aud, and exp.
- In Repeater, tamper with alg to none and remove the signature. Observe if the server accepts it. It should not.
- Change aud to a different value and resend. The server should reject tokens with unexpected audience.
- Change exp to a time in the past. The server should reject expired tokens.
- Try changing the signature using a wrong key. The server should reject invalid signatures.
To test CSRF:
- Confirm cookies carry HttpOnly and SameSite=Lax or Strict where possible.
- Identify a state-changing POST route such as /settings or /transfer.
- In Repeater, send the request without CSRF token or with an invalid token. The server should reject it.
- In a separate browser tab without your origin, craft a form that submits to the target route. Observe whether SameSite blocks the cookie from being sent. If sent, the CSRF token should still block the action.
To test IDOR:
- Capture a request that reads or updates a resource owned by your test user.
- Modify the resource ID to another user’s ID. If you do not know one, create a second account and observe its IDs.
- Send the modified request. The server should reject or return 404 if you lack permission. If it returns data, you found IDOR.
- Repeat across different endpoints, including export or report generation routes. IDOR often hides in less-used features.
To test SSRF:
- Identify endpoints that accept URLs to fetch, such as preview or webhook verification.
- Provide a URL to an internal IP like http://169.254.169.254 or http://127.0.0.1. The server should block it.
- Use Burp Collaborator to generate a unique URL and see if the server makes a request out to it. This detects blind SSRF.
- Try redirects from allowed domains to disallowed destinations. The server should re-check the final destination.
To test XSS:
- Identify inputs that reflect in the DOM or are stored and displayed later.
- Inject harmless markers like
and see where they appear. - Attempt context-aware payloads, such as " onfocus=alert(1) for attributes, or for HTML contexts.
- Verify that output encoding or sanitization neutralizes payloads. Confirm CSP blocks inline script execution.
Practice, labs, and career pathways in web security
Technical depth grows with hands-on practice and structured growth paths. Build a lab to rehearse techniques without risking production. Start with intentionally vulnerable applications like OWASP Juice Shop or DVWA running in containers. Add a reverse proxy and a Burp Suite proxy. Seed test users and data. Practice enumeration, authentication testing, authorization bypass attempts, and SSRF detection. Keep a journal of findings and reproduction steps to develop a repeatable method.
As you progress, connect web security skills with adjacent disciplines. Web exploits often use network quirks, misconfigured cloud services, or weak identity flows. Strengthen those foundations with the overviews in network security basics and cloud security fundamentals. Apply those insights back to your app with tests that inspect CORS, HSTS, TLS, and service-to-service auth. For offensive practice, structure your approach using the methodology in the hands-on penetration testing guide.
Plan your learning and career with intention. Web application security work exists in product teams, security engineering, consulting, and testing roles. Decide if you want to focus on building secure platforms, testing and exploitation, or governance and standards. Map skills to roles, such as code review and SDL for appsec engineers, or recon and Burp mastery for pentesters. Get perspective on roles and growth paths from the cybersecurity career guide, then select a credential plan using our security certifications overview.
Structured study and mentorship shorten the path from curiosity to job-ready. If you want guided practice that blends study with real-world internships, explore the study format and outcomes of our study and internship cyber security program. The program emphasizes doing the work in realistic environments, building a portfolio of findings and fixes, and collaborating with mentors who have shipped secure systems. That combination of practice and accountability is what moves you from reading about attacks to preventing them.
FAQ
Q: How do I choose between server sessions and JWTs for my web app? A: Prefer server-backed sessions with HttpOnly, Secure, SameSite cookies when your architecture allows it. They are simple to revoke and rotate, and browsers enforce key defenses for you. Use JWTs for stateless APIs and distributed systems where self-contained tokens are convenient, but keep access tokens short-lived, store them in HttpOnly cookies if used in browsers, and implement refresh token rotation with server-side tracking. Avoid long-lived bearer tokens and do not put sensitive data in JWT claims.
Q: What is the fastest way to reduce common web vulnerabilities in an existing app? A: Start with high-leverage guardrails that require minimal code changes. Configure secure headers like HSTS, X-Frame-Options or frame-ancestors, X-Content-Type-Options, and a basic CSP. Set cookies to HttpOnly and SameSite. Add centralized authorization middleware that enforces object checks for key resources. Introduce schema validation for request bodies and parameterized queries in the data layer. Run a DAST scan in staging to catch obvious misconfigurations and reflected XSS, then fix findings by category.
Q: How do I prevent IDOR across a large codebase with many endpoints? A: Centralize authorization policy and make data access functions permission-aware. Do not allow fetching objects by ID without passing the subject, and enforce policy in the data layer, not in controllers. Write integration tests that attempt to access other users’ resources for each route type. Add linters or code review checklists that block patterns like using user_id from a request body. Use API gateways or service meshes to attach identity and context, but still enforce authorization in the service that owns the data.
Q: What does a realistic CSP look like for a modern SPA? A: A good starting point is default-src 'self', script-src 'self' plus a very small set of approved CDNs, style-src 'self' 'unsafe-inline' initially if your framework requires it, but move to nonces or hashes where possible. Block object-src entirely, control frame-ancestors to prevent framing, and allow images from self and your CDN. Use nonces for inline scripts you cannot avoid. Monitor with a report endpoint and iterate. Do not attempt to enforce a policy that your build pipeline cannot support.
Q: Can rate limiting replace CSRF tokens in cookie-authenticated apps? A: No. Rate limiting controls volume, not intent. CSRF exploits the browser automatically sending cookies to your site. Without a server-verified anti-CSRF token, an attacker can trick users into performing single, critical actions like changing email or transferring funds. SameSite cookies help, but they are not a complete defense for all flows and browsers. Use tokens for state-changing requests and validate them server-side, even if you also rate limit.
Q: How do I make SSRF less dangerous in a cloud-native app that must fetch URLs? A: Combine network and application controls. At the network layer, block egress to internal IP ranges and metadata endpoints unless explicitly required, and use features like AWS IMDSv2. At the application layer, parse and validate URLs strictly, block non-HTTP schemes, resolve DNS and check resolved IPs against allowlists, and restrict redirects. Use a dedicated fetcher service with a constrained network profile. Log and alert on attempts to reach private ranges.
Q: Which testing tools should I start with if I have no security automation today? A: Begin with SCA to address vulnerable dependencies and SAST to block high-risk code patterns. Add secret scanning. In parallel, set up a staging environment and run a targeted DAST against high-value flows. As your test suite matures, add IAST during integration tests for higher-fidelity findings. Use Burp Suite for manual exploration and to validate and reproduce issues that tools flag. Prioritize a small set of policies and rules that have high signal to build trust.
Q: Where can I get a structured pathway to grow from basics to hands-on proficiency? A: Use our domain overviews like the cybersecurity learning hub to orient yourself, then practice using the hands-on penetration testing guide and adjacent resources like cloud security fundamentals. If you prefer guided study with practical experience, review the outcomes and curriculum of the study and internship cyber security program. For role clarity and credentials, consult the cybersecurity career guide and the security certifications overview.
