Background Concepts

Expand any topic below for the same background material reused across the lab writeups. These are the building blocks that sit behind JWT, OAuth, SSRF, smuggling, PHP deserialization, and prototype pollution labs.

DOM-based

DOM Clobbering click to expand

In the browser, window is the global object. Some names on it can be read two ways: as window.color and as the bare name color. That is not true of every global.

A top-level var becomes a property of window:

 color = "blue"
.color // "blue"

A top-level let or const does not. The name exists in the script, but window has no matching property:

 accent = "red"
.accent // undefined

The other direction also works. A property assigned onto window is visible as a bare global:

.lang = "en"
lang // "en"

So window.settings is not “the variable named settings.” It is a property lookup on the global object. HTML can create those properties too. A top-level const settings = ... would not collide with that. A lookup of window.settings can.

A page script often needs a few defaults (theme, language, a default image path). Another script on the same page might already have put a real config object on window under that name. So a common pattern is:

 settings = .settings || { theme: "light", lang: "en" }

|| means “if the left side is missing or empty, use the right side.” If nothing defined window.settings, the value is undefined, which is falsy, and the hardcoded object ({ theme: "light", lang: "en" }) is used. If a trusted script previously set window.settings = { anything: "anything" }, that object is used instead.

This check only distinguishes “nothing on window.settings” from “something truthy.” It does not check that the value is actually a config object. A DOM node is also truthy, so it would pass the same test.

Chromium (Chrome, Edge, and other Blink browsers) has an extra rule. An element with id="settings" is also exposed as window.settings. The value is the node, not a config object. Nodes are truthy, so the || fallback never runs. HTML has overwritten the name the script was about to use. That overwrite is . No <script> tag is required. Attacker-controlled markup is enough.

One element clobbers a fallback

HTML in the document:

<a id="settings"></a>

Then the same lookup as above:

 settings = .settings || { theme: "light", lang: "en" }

console.log(settings.theme)

window.settings is the <a> element. settings.theme is not "light". The script is reading a property on an anchor, not on the fallback object.

Two elements can fake an object shape

When two elements share the same id, Chromium often exposes window.<id> as an HTMLCollection (a list of both nodes). Named items in that collection can be read like object properties. On <a> tags, a name attribute can surface as a property whose value comes from href.

<a id="opts"></a>
<a id="opts" name="theme" href="dark"></a>

In Chromium, this can resolve to a string:

.opts.theme // often "dark" (from the second anchor's href)

No <script> tag was required. Tags that sanitizers often still allow, such as <a>, were enough to make JavaScript see something shaped like { theme: "dark" }.

Quick reference (Chromium)

HTML on the pageWhat JavaScript may read
<div id="foo">window.foo is the element
Two nodes with id="foo", one with name="bar" href="...">window.foo is an HTMLCollection; window.foo.bar is often a string from href

Why it matters

HTML sanitizers may block <script> and inline event handlers while still allowing <a>, <img>, and similar tags. If the application reads defaults from window.someName and later passes the result into a (for example or a URL attribute), clobbering can turn that “safe” markup into .

What happens with Firefox

Firefox still clobbers flat names. One element with id="foo" can show up as window.foo, so a window.foo || fallback check can skip the fallback there too.

What Firefox does not do is the second row of the table. Duplicate ids stay a single node (the first match), not an HTMLCollection. window.foo.bar from a name / href on another element with the same id does not resolve the way it does in Chromium. The HTML is still in the document. The fake object shape is not.

Knowledge Check

5 questions
DOM Sink click to expand

A is a place in the client-side JavaScript where data from outside the script is processed in a dangerous way.
“Sink” means the data flows into something that can change what the browser parses or runs.

The opposite idea is a source: where data enters the page. That data can be reflected (it comes straight back in the same response, like a URL parameter or search box) or stored (the server saved it earlier and sends it later, like a comment from a previous visit).

A DOM bug is usually a source connected to a sink.

Common sinks

SinkWhat goes wrong
element.innerHTML = ...Browser parses the string as HTML. Tags and attributes in the data can run scripts or handlers.
element.outerHTML = ...Same parser pass, replaces the element itself.
insertAdjacentHTML(...)Same parser pass, inserts HTML at a chosen position.
document.write(...)Writes markup into the open document stream.
eval(...), setTimeout(string, ...)Runs the string as JavaScript code.
location = ..., location.href = ...Can jump to javascript: URLs in some contexts.

Not every DOM write is a sink. Assigning to textContent or innerText treats input as plain text. The browser does not parse HTML tags from the string, so <script> stays literal text on screen.

Why innerHTML is a classic sink

A search results page might build a message like this:

 searchQuery = new URLSearchParams(location.search).get('q')

.('results'). = '<p>Results for: ' + searchQuery + '</p>'

If ?q= contains </p><img src=x onerror=alert(1)><p>, the browser will parse the HTML closing the paragraph early. The <img> is no longer inside the string as text. It becomes a real element in the DOM, and the onerror handler will run the alert.

That search example is reflected DOM XSS. The source is the ?q= parameter in the URL. The sink is the line in client-side JavaScript. The server did not need to echo the payload in the HTML response.

If the same query value were already rendered as HTML in the server response (for example <p>Results for: PAYLOAD</p> in the page source), it would be ordinary reflected XSS instead. The browser would parse that markup on the initial load. No client-side sink would be involved.

Knowledge Check

5 questions

Fundamentals

DOM (Document Object Model) click to expand

The (Document Object Model) is the browser’s live representation of a page. When the browser receives HTML, a parser reads the tags and builds a tree of objects in memory: elements, attributes, and text nodes. That tree is the DOM.

What you see on screen and what JavaScript can touch are both backed by this tree. The initial HTML from the server is only the starting point. Scripts can add, remove, or change nodes afterward.

How HTML becomes a tree

Take a small snippet:

<div id="comments">
  <p>Hello</p>
</div>

After parsing, the DOM might look like this in plain terms:

  • a div element with id="comments"
    • a p element inside it
      • a text node containing Hello

JavaScript can walk that tree with APIs like document.getElementById("comments") and read or change what is there. In the previous example, to replace the Hello text inside the paragraph:

 paragraph = .("#comments p")
paragraph.textContent = "Updated from JavaScript"

After that runs, the tree in the browser’s memory (and the text you see on the page) looks like this:

<div id="comments">
  <p>Updated from JavaScript</p>
</div>

You changed the live DOM that the page is showing right now. If you reload the page, the browser parses the original HTML again and the paragraph goes back to Hello.

Parsing happens more than once

Parsing is not a one-time event on first page load. Whenever code assigns an HTML string to a property like , the browser runs the parser again on that string and creates new nodes from it.

So if a script builds:

element. = '<img src="' + userInput + '">'

the browser does not treat userInput as plain text. It parses the whole string as HTML. A quote or tag character in the wrong place can change which attributes exist on the resulting element.

Knowledge Check

5 questions
JWT Fundamentals click to expand

What is JWT (JSON Web Token)?

is a compact, URL-safe means of representing Claims* to be transferred between two parties. It’s commonly used for authentication and information exchange in web applications. It uses base64 encoding to ensure URL-safe transmission.

A JWT consists of three parts separated by dots (.):

header.payload.signature

For example:

..

Breaking down each part:

1. Header (Base64URL encoded JSON):

{
  "kid": "170c351e-58cc-4e4b-9116-6e7924337580",
  "alg": "HS256"
}
  • alg: The signing algorithm (HS256 = HMAC-SHA256)
  • kid: Key ID (optional identifier for the signing key)

2. Payload (Base64URL encoded JSON):

{
  "iss": "portswigger",
  "exp": 1772198957,
  "sub": "wiener"
}
  • iss: Issuer of the token
  • exp: Expiration timestamp
  • sub: Subject (usually the username)

3. Signature: The signature is created by taking the encoded header and payload, and signing them with a secret key using the algorithm specified in the header:

# Pseudocode

message = base64UrlEncode(header) + "." + base64UrlEncode(payload) 

key = JWT_SECRET # It could be a password, API key, UUID, or any other string

signature = HMACSHA256(message, key)  # This is just the signature part, not the complete JWT

The signature ensures that the token hasn’t been tampered with. If an attacker modifies the header or payload, the signature won’t match unless they know the secret key.

The complete JWT would be constructed as:

# Pseudocode

jwt = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature

Knowledge Check

5 questions
Understanding OAuth 2.0 & Authorization Flows click to expand

What is OAuth 2.0?

is an authorization framework that allows third-party applications to access a user’s resources without exposing their credentials. Instead of sharing passwords, OAuth uses authorization codes and access tokens to grant limited access.

Think of OAuth like giving someone a temporary valet key for your car:

  • Without OAuth: You give someone your main car key (password). They can drive your car, open the trunk, access the glove box. Basically everything. If they lose it or steal it, they have full access to your car.

  • With OAuth: You give them a special valet key (access token) that only opens the driver’s door and starts the engine. They can’t access the trunk or glove box. The key expires after a short time, and only works for specific cars (limited permissions).

The key players in an OAuth flow are:

  • Resource Owner: The user who owns the data (e.g., the admin)
  • Client Application: The website that wants to access the user’s data (e.g., the blog)
  • OAuth Provider (Authorization Server): The service that authenticates the user and issues tokens (e.g., the social media login server)

Real-world example: When you click “Log in with Google” on a website, that website is the Client Application, Google is the OAuth Provider, and you are the Resource Owner. The website never sees your Google password. It just gets a temporary token to access basic profile information.

The Authorization Code Flow

The most common OAuth flow works like this:

Step-by-step breakdown:

  1. User clicks “Log in with Social Media” on the client app (e.g., clicking “Log in with Google” on a blog)

  2. Client app redirects user to the OAuth provider’s /auth endpoint with several parameters:

    • client_id: identifies the client application (like “this is the blog website”)
    • redirect_uri: where to send the user back after authorization (like “send them back to https://<blog>.com/oauth-callback”)
    • response_type=code: requests an authorization code (not direct access)
    • scope: what permissions are requested (like “I just need your email and profile picture”)
  3. User authenticates with the OAuth provider (enters their Google/Facebook credentials)

  4. User consents to the requested permissions (sees a screen like “Allow this blog to access your email and profile?”)

  5. OAuth provider redirects user back to the redirect_uri with an authorization code: https://<blog>.com/oauth-callback?code=XYZ123ABC This is like getting a receipt that says “the user approved this request”

  6. Client app exchanges the code for an access token (server-side, the blog sends the code back to Google saying “here’s the receipt, now give me the actual key”)

  7. Client app uses the access token to access user resources (the blog can now fetch the user’s profile info from Google using the temporary key)

Why this two-step process? The authorization code is short-lived and single-use. Even if an attacker intercepts it, they can only use it once and it expires quickly. The access token is what actually grants access, but it’s exchanged server-to-server, never exposed to the user’s browser.

Knowledge Check

5 questions

HTTP Request Smuggling

CL.TE & Differential Responses click to expand

CL.TE Smuggling & Differential Responses

In a vulnerability the front-end trusts while the back-end trusts : .

A minimal request looks like:

 / HTTP/1.1
: vulnerable-website.com
: 13
: 

0

SMUGGLED

What each server sees:

  1. Front-end (CL): body is 13 bytes, so it forwards everything through SMUGGLED
  2. Back-end (TE): sees chunk size 0, so the request ends immediately; leftover bytes SMUGGLED stay in the socket buffer
  3. Those leftover bytes are prepended to whatever request arrives next on that back-end connection

Confirming with differential responses

Timing delays can hint at smuggling, but a stronger proof is to force a different response than a normal request would get.

The usual pattern:

  1. Send an attack request that smuggles GET /404 HTTP/1.1... into the back-end buffer
  2. Immediately send a second request (ideally on a different client connection, same URL so load balancing still hits the same back-end)
  3. If the second response is 404 Not Found instead of the normal 200 OK for /, the smuggled prefix was applied and CL.TE is confirmed

After a successful smuggle, the back-end effectively processes something like:

 /404 HTTP/1.1
: XPOST / HTTP/1.1
: ...
...

X-Ignore (or any dummy header) absorbs the start of the following real request so header parsing does not blow up before the path /404 is evaluated.

Knowledge Check

5 questions
HTTP Request Smuggling Basics click to expand

What is HTTP Request Smuggling?

tricks a site into mixing up where one HTTP request ends and the next one starts.

Most apps sit behind a front-end (proxy / load balancer) that forwards traffic to a back-end. For speed, the front-end often sends several HTTP/1 requests over the same connection. That only works if both servers agree on where each request stops.

Why HTTP/1 can be unclear:

HTTP/1 has two ways to say how long the body is:

  1. : exact size in bytes
  2. : : body sent in pieces, ending with a zero-size chunk (0\r\n\r\n)

If both headers are present and disagree, one server may use Content-Length and the other Transfer-Encoding. The attacker can then hide extra bytes that the back-end treats as the start of the next request.

Classic variants, including :

VariantFront-end usesBack-end uses
CL.TEContent-LengthTransfer-Encoding
TE.CLTransfer-EncodingContent-Length
TE.TEBoth support TE, but one can be tricked with a weird TE header

Impact can include:

  • Bypassing front-end security controls (rules on the proxy never see the hidden inner request)
  • Poisoning other users’ requests / responses (leftover bytes get stuck onto someone else’s next request)
  • Stealing credentials, tokens, or poisoning caches (capture or rewrite what another user sends or receives)
  • Reaching privileged endpoints (e.g. hide GET /internal so the back-end sees it while the front-end only allowed /, skipping path rewrites or auth headers the front-end would have added)

Knowledge Check

5 questions

JWT

HS256 & Brute-force Attacks click to expand

What is HS256 (HMAC-SHA256)?

HS256 is a symmetric signing algorithm, meaning the same secret key is used to both sign and verify the token. This is different from asymmetric algorithms like RS256, which use a private key to sign and a public key to verify.

The security of HS256 depends entirely on the secrecy and strength of the key. If the key is weak (like “secret”, “password”, or “secret1”), it can be brute-forced.

Why is Brute-forcing Possible?

Since the signature is deterministic (same input + same key = same signature), an attacker can:

  1. Take the ’s header and payload
  2. Try signing it with different secret keys from a wordlist
  3. Compare the generated signature with the original
  4. When they match, the secret key has been found

This is only feasible when the secret is weak and exists in common wordlists.

Knowledge Check

5 questions
JWT Algorithm Confusion Attack click to expand

What is a JWT Algorithm Confusion Attack?

(also known as key confusion) exploits servers that use asymmetric algorithms like RS256 but fail to enforce the expected algorithm when verifying tokens.

How RS256 normally works:

  • The server signs tokens with a private key
  • The server verifies tokens with the corresponding public key
  • The public key is often exposed via a standard endpoint like /jwks.json

The vulnerability:

When a server receives a JWT, it reads the alg header to determine which algorithm to use for verification. If the server doesn’t strictly enforce the expected algorithm, an attacker can:

  1. Obtain the server’s public key (often available at /jwks.json or /.well-known/jwks.json)
  2. Change the alg header from RS256 to HS256
  3. Sign the token using HMAC-SHA256 with the public key as the symmetric secret

The server then sees alg: HS256, picks its “verification key” (which is the RSA public key), and uses it as the HMAC secret exactly matching the attacker’s signature.

Expected header
Confused header
{
"kid": "66a4de73-4f3b-4bc3-af9e-913726ff79e9",
"alg": "RS256"
}
{
"kid": "66a4de73-4f3b-4bc3-af9e-913726ff79e9",
"alg": "HS256"
}
{
"kid": "66a4de73-4f3b-4bc3-af9e-913726ff79e9",
"alg": "HS256"
}
Normal flow: RS256 -> verify with public key (asymmetric)
Attack flow: HS256 -> verify with public key (used as symmetric secret)

This works because the server’s verification code generically passes its “key” to whatever algorithm is specified, without checking that the algorithm matches the key type.

Knowledge Check

5 questions
JWT kid Header & Path Traversal click to expand

What is the kid Header Parameter?

The (Key ID) is an optional header parameter in a that tells the server which key to use to verify the token’s signature. This is useful when a server manages multiple signing keys.

{
  "kid": "dade186d-6d19-4584-ab27-737975e1611f",
  "alg": "HS256"
}

The server receives the JWT, reads the kid value, and uses it to look up the corresponding signing key. The implementation of this lookup varies. Some servers use a database, others use the filesystem.

Path Traversal via kid

When the server fetches the signing key from the filesystem using the kid value, it becomes vulnerable to if the input is not sanitized. An attacker can manipulate kid to point to any file with known contents:

Normal kid
Traversed kid
{
"kid": "dade186d-6d19-4584-ab27-737975e1611f",
"alg": "HS256"
}
{
"kid": "../../../../../../../dev/null",
"alg": "HS256"
}
{
"kid": "../../../../../../../dev/null",
"alg": "HS256"
}

On Linux, /dev/null is a special file that always returns empty content. By pointing kid to it, the server will use an empty string as the signing key. The attacker can then sign their forged JWT with an empty string, and the server will accept it as valid.

Knowledge Check

5 questions
JWT Unverified Signature Vulnerability click to expand

JWT Unverified Signature Vulnerability

When a server receives a , it should always verify the signature before trusting the token’s contents. However, some implementations fail to do this properly.

The JWT specification is flexible. It defines how tokens should be structured and signed, but the actual signature verification is left to the application’s code. This creates a dangerous gap: if the server only decodes the JWT , an attacker can modify the payload at will.

How the attack works:

1. Attacker logs in and receives a valid JWT
2. Attacker decodes the JWT payload (base64)
3. Attacker modifies the payload (e.g., changes "sub": "wiener" to "sub": "administrator")
4. Attacker re-encodes the payload (base64)
5. Attacker sends the modified JWT to the server, which accepts it without checking the signature

The token’s header and signature can remain completely untouched. Only the payload needs to be modified:

Original payload
Tampered payload
{
"iss": "portswigger",
"exp": 1773866751,
"sub": "wiener"
}
{
"iss": "portswigger",
"exp": 1773866751,
"sub": "administrator"
}
{
"iss": "portswigger",
"exp": 1773866751,
"sub": "administrator"
}

Alternative attack: Changing algorithm to “none”

Some JWT implementations also accept the "none" algorithm, which completely disables signature verification. Attackers can:

1. Change the header's "alg" from "RS256" or "HS256" to "none"
2. Remove the signature part entirely (or keep any value)
3. The server accepts the token without any signature validation

This creates a token like: header.payload. (no signature) or header.payload.invalid_signature.

Why does this happen?

  • Libraries often provide separate methods for decoding and verifying JWTs
  • A developer might accidentally use decode() instead of verify(), which skips signature validation
  • The application trusts the token blindly once it can parse the JSON structure
  • Some libraries accept the "none" algorithm for unsigned tokens, intended for specific use cases but dangerous if not properly restricted

Knowledge Check

5 questions

Misc

Backup Files & Source Code Leaks click to expand

What is a Backup File (~)?

Many text editors (like Vim, Emacs, and others) automatically create backup copies of files by appending a tilde (~) to the filename. For example, editing CustomTemplate.php creates CustomTemplate.php~. These backup files often get accidentally deployed to production servers and can leak source code to anyone who requests them, depending on the configuration of the web server it may serve them as plain text.

Knowledge Check

5 questions

OAuth

OAuth redirect_uri Validation click to expand

Why is redirect_uri Validation Critical?

The redirect_uri parameter specifies where the provider should send the authorization code after user authentication. Without strict validation, an attacker can modify this parameter to point to a server they control. When the victim authenticates, their authorization code is intercepted by the attacker instead of being delivered to the legitimate application.

In OAuth terms:

  • Secure: The OAuth provider only redirects to pre-registered URLs (like https://<blog>.com/oauth-callback)
  • Vulnerable: The OAuth provider redirects to whatever URL the attacker provides (like https://<attacker-site>.com/steal-code)

Why is this so dangerous? Authorization codes are bearer tokens. Whoever has the code can exchange it for access to the victim’s account. It’s like finding someone’s car keys. If you have them, you can drive the car.

Knowledge Check

5 questions

Object Injection

Understanding PHP Serialization click to expand

What is Serialization?

Serialization is the process of converting an object (a data structure in memory) into a format that can be stored or transmitted, like a string. is the reverse: turning that string back into an object. In PHP, this is done with serialize() and unserialize().

For example, a PHP object like:

$user = new User();
$user->username = "wiener";
$user->access_token = "abc123";

Gets serialized into:

O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:6:"abc123";}

Breaking down the format:

  • O:4:"User" — Object of class name length 4, named “User”
  • :2: — has 2 properties
  • s:8:"username" — string property name of length 8: “username”
  • s:6:"wiener" — string value of length 6: “wiener”

When an application stores serialized objects in cookies (like session tokens), an attacker can tamper with them. If the server blindly deserializes user-controlled input, it will reconstruct whatever object the attacker provides.

Knowledge Check

5 questions

PHP

PHP Magic Methods click to expand

What are PHP Magic Methods?

PHP has special methods called magic methods that are automatically invoked at certain points in an object’s lifecycle. They always start with a double underscore (__). The most relevant ones for attacks are:

  • __construct() — called when an object is created (new ClassName())
  • __destruct() — called when an object is destroyed (goes out of scope, script ends, or garbage collected)
  • __wakeup() — called when an object is deserialized (unserialize())
  • __toString() — called when an object is treated as a string

The dangerous one here is __destruct(). When PHP deserializes an object from a cookie, it creates that object in memory. When the request finishes processing, PHP’s garbage collector destroys the object, which automatically triggers __destruct(). The attacker doesn’t need to call it — PHP does it for them.

Knowledge Check

5 questions

Prototype Pollution

Javascript Prototype click to expand

Most JavaScript objects have a link to a special object called its prototype

That link is not a copy of properties. It is a fallback: when you read a key that the object does not own, the engine looks for it on the prototype.

Each prototype can link to another one in turn. Those links form a prototype chain, which ends at null.

So a missing property is searched in this order:

  1. the object itself (own properties)
  2. its prototype
  3. that prototype’s prototype, and so on, until either finds it or finds null and returns the famous undefined

Prototype chain behavior examples

A lookup can walk more than one link. Here user inherits from roleDefaults, which still inherits from Object.prototype:

 roleDefaults = {
  isAdmin: false
}

const user = {
  username: "carlos",
  : roleDefaults
}

console.log(user.username) // "carlos" (own)
console.log(user.isAdmin)  // false    (from roleDefaults)
console.log(user.toString) // [Function: toString] (from Object.prototype but was searched first in user and roleDefaults)

// The full chain:
// user -> roleDefaults -> Object.prototype -> null

A few more short examples

Undefined property default behavior:

 user = {
  username: "carlos"
}

console.log(user.isAdmin) // undefined

Undefined property but present on the prototype chain:

 user = {
  username: "carlos",
  : {
    isAdmin: true
  }
}

console.log(user.isAdmin) // true

Priority of properties:
Own properties always win over inherited ones. If the object already has the key, the chain is not consulted

 user = {
  username: "carlos",
  isAdmin: false,
  : {
    isAdmin: true
  }
}

console.log(user.isAdmin) // false

Knowledge Check

5 questions
Node.js execArgv Gadget click to expand

Node.js execArgv as a Gadget

When you start Node yourself, you can pass CLI flags before the script:

node --inspect app.js
node --require ./preload.js app.js
node --eval "console.log(1)"

Those flags change how the Node process boots: enable the debugger, preload a module, or run inline JavaScript with --eval / -e.

child_process.fork() and spawn() can pass the same kind of flags to a child Node process through an options field named execArgv. It is just an array of strings, for example ["--inspect"] or ["--eval=console.log(1)"].

 {  } = ("")

("./worker.js", [], {
  : ["--eval=console.log('child boot')"]
})

If application code builds an options object and never sets its own execArgv, a normal property read still walks the prototype chain:

 options = {}

options. // undefined

. = ["--eval=console.log('polluted')"]

options. // ["--eval=console.log('polluted')"]

After , fork(script, args, options) can start the child with attacker-controlled Node flags. --eval is the interesting one: the child runs arbitrary JavaScript during startup. From that JavaScript you can call require("child_process").execSync(...) and run OS commands.

So the gadget is not “fork is always RCE”. The gadget is: pollute execArgv, then hit any code path that uses fork or spawn with an options object that inherits your value.

Knowledge Check

5 questions
Prototype Pollution Basics click to expand

exploits how JavaScript objects inherit properties. With controlled input, an attacker sets properties on an object’s prototype (the shared fallback other objects also use). Those objects then see the new properties even though they never defined them, which can change how the application behaves.

The usual high-impact target is Object.prototype, because most plain objects inherit from it. The attack does not always need that root, though.

Any prototype that sits on the chain of objects the application trusts is enough.

Polluting User.prototype, a shared config prototype, or another intermediate link can flip checks like if (user.isAdmin) for that lineage alone, even while unrelated objects stay unaffected.

Prototype pollution can happen client-side in the browser, or server-side if the backend runs a JavaScript runtime (usually Node.js).

Example payloads

These are common shapes attackers send when an app merges or parses user input unsafely.

Via __proto__:

{
  "": {
    "isAdmin": true
  }
}

Same idea in a query or URL (some client-side parsers turn nested keys into objects):

?__proto__[isAdmin]=true
#__proto__[isAdmin]=true

Via constructor.prototype:

{
  "": {
    "": {
      "isAdmin": true
    }
  }
}

Payload explaination:

Every object has a constructor that points at the function used to create it. Writing to constructor.prototype pollutes that function’s prototype, so later instances inherit the property.

class User {
  username = ""
}

 user1 = new User()

user1...isAdmin = true // same as: User.prototype.isAdmin = true

const user2 = new User()

console.log(user2.isAdmin) // true

const somethingElse = {}

console.log(somethingElse.isAdmin) // undefined (different prototype chain)

Knowledge Check

5 questions

SSRF & Open Redirection

Open Redirection Vulnerabilities click to expand

What is Open Redirection?

is a vulnerability where an application accepts user-controlled input that determines where to redirect the user, without properly validating the destination URL. This allows attackers to redirect users to malicious websites.

How open redirection works:

  1. Application has a redirect endpoint like ?redirect=https://example.com
  2. The parameter value is used directly in a Location header or JavaScript redirect
  3. Attacker replaces the URL with a malicious site
  4. Users are redirected to the attacker’s site

Common vulnerable patterns:

 /redirect?url=https://<attacker-domain>.com
GET /login?return=https://<attacker-domain>.com
GET /next?target=https://<attacker-domain>.com

Why open redirection is dangerous:

  • Phishing attacks - redirect users to fake login pages
  • Bypassing filters - use the legitimate domain to bypass URL filters
  • Chain attacks - combine with other vulnerabilities like

Knowledge Check

5 questions
Server-Side Request Forgery (SSRF) click to expand

What is Server-Side Request Forgery (SSRF)?

is a vulnerability that allows an attacker to make the server-side application send HTTP requests to an arbitrary domain of the attacker’s choosing. This can be used to:

  • Access internal services that are not directly exposed to the internet
  • Bypass authentication by accessing internal admin interfaces
  • Read sensitive files from internal systems
  • Extract data from internal databases or APIs

How SSRF works:

  1. The application accepts a URL as a parameter (example: ?url=https://example.com)
  2. The server makes an HTTP request to that URL
  3. The server processes the response (blind SSRF) or returns it to the user (non-blind SSRF)
  4. An attacker replaces the URL with an internal address (example: http://localhost/admin)

Common targets for SSRF:

  • localhost or 127.0.0.1 - the server itself
  • 192.168.x.x or 10.x.x.x - internal network ranges
  • 169.254.x.x - AWS metadata service
  • Internal APIs, databases, or admin panels

Knowledge Check

5 questions
SSRF + Open Redirection Chain Attack click to expand

Open Redirection + SSRF Chain Attack

When an application can make arbitrary requests with protections and also has an vulnerability, attackers can chain these to bypass the SSRF filters:

Example attack flow:

  1. SSRF protection blocks: http://localhost/admin - direct internal URLs are filtered
  2. But allows: https://<victim-domain>/redirect?url=http://localhost/admin - external domains (victim’s domain) pass validation
  3. Open redirection redirects: The victim’s site redirects the request to the internal target since it’s made server-side
  4. Result: SSRF to internal admin interface bypassed through request redirection making the internal request

Why this works:

  • The SSRF filter only checks if the initial URL is “safe” (external domain)
  • It doesn’t follow redirects to see the final destination
  • The open redirection vulnerability acts as a proxy to reach internal targets

Knowledge Check

5 questions