Application Security / Web Vulnerabilities

Web Application Vulnerabilities & OWASP Top 10

Most web application vulnerabilities reduce to one mechanism: somewhere in the request path, data supplied by a user is handed to an interpreter that reads part of it as an instruction. A SQL parser, a browser's HTML parser, an operating system shell, an object deserializer, and a URL fetcher are all interpreters, and each fails the same way. The defenses differ because the interpreters differ, but the question to ask at every boundary is the same one — is this value being passed as data, or is it being concatenated into something that will be parsed?

The OWASP Top 10 is the usual entry point into this subject. It is an awareness document reflecting a community consensus and contributed vulnerability data — not a standard, a certification scheme, or a compliance requirement. OWASP’s own verification standard, ASVS, is the artifact intended for testing against. Treat the Top 10 as a map of where the losses concentrate, then verify against ASVS.

Three panels. Injection and cross-site scripting: string concatenation into a parser, and script injected into the browser DOM, remediated by parameterized queries and context-aware encoding. SSRF and insecure deserialization: the server coerced into fetching an internal metadata endpoint, and untrusted object streams running gadget chains, remediated by destination allowlisting with IMDSv2 and by safe data formats. Memory safety: the four corruption classes, what Rust and Go change, and the C and C++ hardening flags, with a footer noting that the guarantee covers safe-subset code only.

The four vulnerability families on this page, each paired with the interpreter it abuses and the control that closes it

The OWASP Top 10:2025 categories

The most current released version is the OWASP Top 10:2025, published by the OWASP Foundation and linked from the project page. The ten categories:

ID Category What it covers
A01:2025 Broken Access Control A user acts outside their intended permissions — reading another user’s object, invoking an administrative function, or elevating privilege. Server-Side Request Forgery was a standalone category in 2021 and is folded into A01 in 2025, on the reasoning that SSRF is the server being coerced past its own access boundary.
A02:2025 Security Misconfiguration Insecure defaults, unnecessary features enabled, verbose errors, permissive CORS, missing hardening. Moved up from fifth in 2021 to second.
A03:2025 Software Supply Chain Failures New in 2025. Expands the 2021 Vulnerable and Outdated Components category to the whole ecosystem — dependencies, build systems, and distribution infrastructure.
A04:2025 Cryptographic Failures Data exposed through absent, weak, or misapplied cryptography. Dropped from second to fourth.
A05:2025 Injection Untrusted data interpreted as part of a command or query. Includes SQL, NoSQL, OS command, LDAP, expression-language injection, and cross-site scripting. Dropped from third to fifth.
A06:2025 Insecure Design Missing or ineffective control design, as distinct from a defective implementation of a sound design.
A07:2025 Authentication Failures Renamed from Identification and Authentication Failures. Credential stuffing, weak recovery flows, session fixation, absent brute-force limits.
A08:2025 Software or Data Integrity Failures Code and data accepted without integrity verification — unsigned updates, untrusted deserialization, and CI/CD pipelines that trust unverified input.
A09:2025 Security Logging & Alerting Failures Renamed from Security Logging and Monitoring Failures. Breaches that are not detected, or are detected too late, because events were not recorded or not alerted on.
A10:2025 Mishandling of Exceptional Conditions New in 2025. Improper error handling, logic errors, and failing open when an abnormal condition occurs.

Two consequences worth carrying forward. First, SSRF is now framed as an access-control failure, which is the more useful mental model — the fix is bounding where the server may go, not sanitizing a string. Second, A03 and A08 push most software supply chain material off this page; that subject is covered in Software Bill of Materials (SBOM) & VEX, SLSA & Build Provenance Attestations, and CI/CD Pipeline Security & Workload Identity. Access control as a model — RBAC, ABAC, ReBAC — is in Authorization Models, and object-level enforcement in APIs is in API & Microservice Security.

Injection: where a value becomes an instruction

Injection is the concatenation of untrusted input into a string that an interpreter later parses. The parser cannot distinguish the developer’s intent from the attacker’s, because by the time it sees the string, both are the same text.

SQL injection

# VULNERABLE: the input becomes part of the query text before the parser sees it.
cursor.execute(
    f"SELECT * FROM users WHERE username = '{user_input}' AND password = '{pass_input}'"
)

Submitting ' OR '1'='1 as user_input closes the literal and appends a tautology, so the WHERE clause matches every row.

# SECURE: the query text is fixed; values are bound separately and are never parsed as SQL.
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (user_input, pass_input),
)

The distinction is not “escaping versus not escaping” — it is that the query structure is sent to the database independently of the values. Two limits are worth stating:

OS command injection

Passing a string to a shell means the shell parses it, so ;, |, &&, backticks, and $() are all operators.

# VULNERABLE: the shell parses the whole string.
os.system(f"ping -c 1 {host}")

# SECURE: no shell; the argument vector is passed to execve directly.
subprocess.run(["ping", "-c", "1", host], shell=False, check=True)

Passing an argument list with no shell removes the metacharacter problem entirely. Where a shell genuinely cannot be avoided, allowlist the input against an explicit pattern — escaping shell metacharacters correctly across shells and locales is difficult to get right and easy to get wrong.

Cross-site scripting: three delivery paths, one sink

Cross-site scripting (XSS) is injection where the interpreter is the victim’s browser. The attacker’s script runs inside the origin, so it inherits that origin’s cookies, storage, and ability to make same-origin requests.

Three delivery paths converge on one browser. Reflected XSS travels in a crafted link and returns in the immediate response; stored XSS is written to the database once and served to every later viewer; DOM-based XSS never reaches the server, moving from a client-side source such as location.search into an unsafe sink such as innerHTML. All three end at the same point — the browser parses attacker text as script inside the origin — and a footer contrasts the server-side fix, context-aware encoding on output, with the client-side fix, assigning to textContent instead of innerHTML.

Three delivery paths, one failure: the browser parses attacker-supplied text as script inside the origin

  1. Reflected XSS — the payload travels in the request (typically a query parameter) and is echoed into the immediate response. It needs the victim to follow a crafted link.
  2. Stored XSS — the payload is persisted server-side and served to every subsequent viewer. No per-victim delivery step is needed, which makes it the more severe of the two.
  3. DOM-based XSS — the payload never has to reach the server. Client-side JavaScript reads a source it does not control (location.search, location.hash, postMessage data) and writes it to a sink that parses HTML or evaluates code (innerHTML, outerHTML, document.write, eval, setTimeout with a string). Server-side output encoding does not touch this path.

Encoding has to match the sink

Escaping is context-dependent. The same string is safe in one position and executable in another.

Insertion context Correct handling
HTML text node Encode & as &amp;, < as &lt;, > as &gt;, " as &quot;, and ' as &#x27;.
Quoted HTML attribute Quote the attribute, then encode every non-alphanumeric character below U+00FF as &#xHH;. Never place untrusted data in an unquoted attribute — a space or slash ends the attribute and starts a new one.
Inside a <script> block Prefer not to. Where unavoidable, emit the value with Unicode escapes (\uXXXX). JSON.stringify() alone is not sufficient here: its output can contain </script>, U+2028, and U+2029, each of which breaks out of or terminates the script context.
URL attribute (href, src) Validate the scheme against an allowlist before encoding. Percent-encoding does not stop javascript: — the scheme is read before the path.
CSS value Encode non-alphanumerics as \HH, and reject expression() and url() containing untrusted input.

Three structural defenses do more work than escaping rules:

HttpOnly on session cookies stops script from reading the cookie. It does not stop script from using it: an XSS payload can still issue same-origin requests that the browser attaches the cookie to. It reduces the impact; it does not contain the flaw.

Server-side request forgery: bounding where the server may go

In SSRF the attacker supplies a URL and the server fetches it. The server’s network position is the prize — it can usually reach an internal network, a service mesh, and a cloud metadata endpoint that the attacker cannot reach directly.

The canonical target is the link-local instance metadata service:

http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>

On an unprotected EC2 instance this returns temporary IAM credentials. Layered controls, in the order they should be applied:

  1. Allowlist the destination. Restrict the scheme to https (blocking file:, gopher:, dict:, ftp:) and the host to an explicit set. An allowlist of permitted destinations is the only control here that fails closed; a blocklist of forbidden ones does not.
  2. Re-check the resolved address, not the hostname. Resolve DNS, then verify the resolved IP is not loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10), private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7), or otherwise internal — and connect to that verified address. Checking the hostname alone loses to DNS rebinding, where the name resolves to a permitted address at validation time and an internal one at connection time.
  3. Do not follow redirects, or re-run the full check on every hop. A permitted host answering 302 Location: http://169.254.169.254/ bypasses a validator that only inspected the original URL.
  4. Enforce IMDSv2 on EC2. IMDSv2 requires a PUT to obtain a session token, then an X-aws-ec2-metadata-token header on each request, and sets a low IP TTL. A basic SSRF that only issues GET requests to an attacker-chosen URL cannot satisfy it. Azure IMDS requires Metadata: true and GCP requires Metadata-Flavor: Google for the same reason.
  5. Filter egress at the network. An egress proxy or security-group rule that denies the metadata range and internal ranges outright bounds the damage when application-layer validation is bypassed.

Note the framing change: because A01:2025 absorbs SSRF, the review question is where is this server permitted to go, which is an authorization question, rather than is this string clean.

Insecure deserialization

Native deserializers do not merely parse data — they reconstruct objects, and reconstruction runs code. Java’s ObjectInputStream invokes readObject, Python’s pickle invokes __reduce__, and PHP’s unserialize invokes __wakeup and __destruct. An attacker who controls the byte stream chains together methods already present in the application’s dependencies (a gadget chain) to reach code execution, without needing any vulnerability in those libraries individually.

This category maps to A08:2025 Software or Data Integrity Failures.

Memory safety: what Rust and Go actually guarantee

Memory-corruption bugs are not web-specific, but they sit underneath the web stack — in TLS terminators, image and font parsers, XML libraries, and the runtimes themselves — so they end up in the exploit path of web applications.

The frequently quoted proportion is real but is often attached to the wrong claim. Chromium reports that around 70% of its high-severity security bugs are memory-safety problems, from an analysis of 912 high or critical severity bugs affecting the Stable channel since 2015. Microsoft has reported a similar proportion of the CVEs it assigns annually. Separately, CISA and partners found that 52% of 172 critical open source projects contained code written in memory-unsafe languages, and 55% of the total lines of code across those projects were memory-unsafe. These are three different measurements; none of them is a general statement about “critical patches.”

Corruption class Root cause What a memory-safe language changes
Buffer overflow (spatial) A write passes the end of an allocation into adjacent memory. Rust and Go bounds-check indexing. The check is at runtime for dynamic indices — the compiler elides it only where it can prove the index is in range — and a failed check panics rather than corrupting memory.
Use-after-free (temporal) A pointer is dereferenced after the allocation it names has been freed. Rust rejects it at compile time through ownership and the borrow checker. Go prevents it with garbage collection: an object is not collected while a reference exists.
Double free free() is called twice on the same pointer, corrupting allocator metadata. Rust frees exactly once when the owner goes out of scope. Go does not expose manual free at all.
Null pointer dereference A null or uninitialized pointer is read or written. Rust has no null; absence is Option<T> and must be handled explicitly. Go still permits a nil dereference, but it panics deterministically rather than corrupting memory — a crash, not a corruption primitive.

What the guarantee does not cover

Stating memory safety as absolute is the common error, and it matters because it changes where review effort goes:

Hardening C and C++ that will not be rewritten

Mitigation Build or platform setting What it does
ASLR Compile and link position-independent (-fPIE -pie) Randomizes image base addresses, so an exploit cannot hardcode target addresses.
DEP / NX Non-executable stack (-Wl,-z,noexecstack), non-executable heap Marks writable pages non-executable, forcing code reuse rather than injected shellcode.
Stack canaries -fstack-protector-strong Detects a linear stack overwrite before the function returns.
RELRO -Wl,-z,relro,-z,now Makes the GOT read-only after relocation, removing a common overwrite target.
Fortified libc -D_FORTIFY_SOURCE=3 -O2 Substitutes bounds-checked variants of common string and memory functions where sizes are known.
Control-flow integrity Clang -fsanitize=cfi, or hardware CFI — Intel CET shadow stacks, ARM BTI and pointer authentication Restricts indirect branch targets, breaking ROP and JOP chains.

Verify what actually shipped rather than what the build script claims, with checksec --file=<binary> or hardening-check.

AddressSanitizer belongs in CI, not in production. ASan is a bug-finding instrumentation with roughly 2x slowdown and a large memory overhead, it is incompatible with static linking, and it is not a mitigation — it detects errors during testing. Run it in test and fuzzing pipelines; do not ship it as a hardening flag.

Cross-site request forgery, and two client-side classes worth knowing

Web application review checklist

The checklist below is a journal working model, not a published audit standard. When auditing a web application, evaluate these eight criteria:

Diagnostic area Evaluation question Verification & audit evidence
Query parameterization Are all database queries constructed with bound parameters, and where query structure is dynamic, is it selected from a fixed allowlist rather than interpolated? SAST results for string-built SQL, ORM raw-query call sites, code review of dynamic ORDER BY and identifier paths.
Shell avoidance Do subprocess calls pass an argument vector with no shell, rather than a composed command string? Grep for shell=True, os.system, exec with a single string; SAST command-injection rules.
Context-correct encoding Is output encoding chosen by insertion context, with framework auto-escaping on and every escape hatch (dangerouslySetInnerHTML, v-html) individually justified? Template engine configuration, an inventory of escape-hatch call sites, DOM sink review for innerHTML.
SSRF destination binding Are outbound fetch destinations allowlisted, is the resolved IP re-checked against loopback, link-local, and private ranges before connecting, and are redirects blocked or re-validated? Fetch wrapper source, DNS-resolution guard tests, egress proxy policy, IMDSv2 enforcement setting.
Deserialization format Is native object deserialization of untrusted input absent, in favor of a schema-validated data format? Static scan for ObjectInputStream, pickle.loads, unserialize, yaml.load without SafeLoader.
Memory-safe boundaries For memory-safe components, are unsafe blocks and FFI boundaries inventoried and separately reviewed, and does CI run the race detector? unsafe block inventory, cgo and JNI call sites, CI configuration for -race.
Binary hardening evidence For shipped C and C++ binaries, are PIE, NX, stack protector, RELRO, and fortification present in the built artifact — with ASan confined to test builds? checksec output on release artifacts, build flag diff between test and release pipelines.
CSRF and cookie posture Do state-changing endpoints require an anti-CSRF token or an equivalent, with session cookies setting SameSite explicitly? Framework CSRF middleware configuration, Set-Cookie header capture, a cross-origin POST test.
What I need to remember

Every one of these is the same failure — untrusted data reaching an interpreter that reads part of it as instruction — so the fix is always structural separation, not filtering. Bind query parameters, pass argument vectors instead of shell strings, encode for the exact insertion context, and never deserialize untrusted input with a native object deserializer. SSRF is an access-control problem in the 2025 list: allowlist the destination, re-check the resolved IP against loopback, link-local and private ranges, block redirects, and enforce IMDSv2. Memory-safe languages remove one bug class in safe-subset code only — unsafe, FFI, and Go data races are still live.

Primary references