1

Explanation

GET-Based State Changes

The most easily forgeable CSRF surface is a state-changing action reachable through GET

A GET request can often be triggered simply by causing the victim's browser to load a URL. No form submission is required, and in many cases no JavaScript or user interaction is needed.

For example:

<img src="https://app.example.com/api/account/delete?confirm=true"
     width="0"
     height="0">

When the browser renders the page, it attempts to load the URL in the src attribute.

Victim visits attacker-controlled page
        ↓
Browser encounters: <img src="https://app.example.com/...">
        ↓
Browser issues a GET request
        ↓
Cookie rules are evaluated
        ↓
If the victim's session cookie is permitted on that request:
Authenticated GET request
        ↓
 State-changing action may occur

This makes state-changing GET endpoints especially dangerous because the attacker may only need to get the target URL loaded.

Depending on the context, browsers and other clients can trigger GET requests through mechanisms such as:

  • Resource-loading elements such as <img> or <iframe>.

  • Ordinary links.

  • Browser prefetching or prerendering behavior.

  • Link-preview systems.

  • Crawlers and automated scanners.

  • Email or messaging clients that inspect URLs.

The exact request behavior and whether authentication cookies accompany the request depend on the browser, request context, and cookie attributes particularly SameSite. But the fundamental problem remains: a state-changing action should not be exposed as an operation that can be triggered merely by loading a URL.

`Get` Should Be Safe:

GET is intended for safe, read-only retrieval operations.

GET
        ↓
Retrieve information
        ↓
No intentional server-side state change

An application that instead exposes an action like:

GET /api/account/delete?confirm=true

turns the URL itself into an action trigger.

URL exists
        ↓
Any system loads the URL
        ↓
Request reaches application
        ↓
Application performs action

State-changing operations should use an appropriate non-safe HTTP method such as POST, PUT, PATCH, or DELETE

However, changing the method from GET to POST does not, by itself, eliminate CSRF.

GET state change

Very easy to trigger by loading a URL
        ↓
POST state change

Harder to trigger in some contexts,
but still forgeable through mechanisms
such as a cross-site form
        ↓
Additional CSRF protection may still be required

Using POST instead of GET is good API design, but it is not itself a CSRF defense.

2

Explanation

POST Requests

The classic CSRF target is a state-changing POST request submitted through an attacker-controlled HTML form, exactly as shown in the transfer example in what is csrf section.

Unlike a simple GET based attack, which is largely limited to constructing a URL and its query parameters, a form-based POST can submit a structured collection of attacker-controlled fields.

<form action="https://bank.example.com/api/transfer"
      method="POST" id="csrf-form">

  <input type="hidden" name="recipient"
         value="attacker-account-001">

  <input type="hidden" name="amount"
         value="5000">

  <input type="hidden" name="memo"
         value="Payment">

  <input type="hidden" name="confirm"
         value="true">
</form>

<script>
  document.getElementById('csrf-form').submit();
</script>

The attacker can populate as many form fields as the target endpoint accepts:

Attacker-controlled form
        ↓
recipient = attacker-account-001
amount    = 5000
memo      = Payment
address   = attacker-controlled value
setting   = attacker-controlled value
        ↓
Form submitted to the real target application
        ↓
Browser evaluates whether the victim's authentication cookies
should accompany the request
        ↓
If accepted by the server: Attacker-chosen action
performed as the victim

This makes traditional form submissions a particularly important CSRF surface. The attacker controls the endpoint, the request method, and the values of the fields the form submits, while the victim's browser may provide the authentication state automatically.

Traditional forms natively support only:

GET and POST

They also submit data using standard browser form encodings rather than arbitrary request bodies or attacker-chosen custom headers.

This distinction becomes important with modern APIs.

For example, an endpoint that requires:

Content-Type: application/json

Authorization: Bearer <token>

X-CSRF-Token: <secret value>

cannot generally be reproduced by simply placing hidden <input> elements inside a cross-site HTML form.

Historically, form-based POST requests were the dominant CSRF surface because server-rendered applications commonly changed state through ordinary HTML forms.

User fills in form
        ↓
Browser submits POST
        ↓
Server changes state

This is why the classic CSRF defensesβ€”particularly synchronizer tokens and later cookie-based controls such as SameSiteβ€”are so closely associated with form submissions.

The underlying rule is:

Changing an action from GET to POST makes it harder to trigger through a simple URL, but it does not prevent CSRF. A cross-site form can still create a valid-looking POST request unless the application requires something the attacker cannot supply.

3

Explanation

JSON APIs

JSON-based endpoints are meaningfully harder to forge with a traditional HTML form, but JSON should not be treated as automatically immune to CSRF.

A normal HTML form can submit only GET or POST requests, and a POST form uses one of a limited set of standard encodings:

application/x-www-form-urlencoded

multipart/form-data

text/plain

A plain HTML form cannot submit:

Content-Type: application/json

For example, if an API genuinely requires:

POST /api/transfer HTTP/1.1
Content-Type: application/json

{
  "toAccount": "attacker-account-001",
  "amount": 5000
}

an attacker cannot reproduce that request using only:

<form>
    <input>
    <input>
</form>

CORS as an Additional Barrier:

JSON requests are harder for an attacker's website to create than ordinary form submissions.

Suppose the victim is logged in to:

bank.example.com

The attacker controls:

evil.example.com

The attacker wants JavaScript on evil.example.com to send this request:

POST https://bank.example.com/api/transfer
Content-Type: application/json

{
  "toAccount": "attacker-account-001",
  "amount": 5000
}

A normal HTML form cannot create this exact request because it cannot set:

Content-Type: application/json

The attacker therefore tries to use JavaScript:

fetch("https://bank.example.com/api/transfer", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    toAccount: "attacker-account-001",
    amount: 5000
  })
});

Because this is a cross-origin request using application/json, the browser usually sends a preliminary request first. This preliminary request is called a CORS preflight.

evil.example.com
        ↓
JavaScript asks the browser to send a JSON request to bank.example.com
        ↓
Browser first asks bank.example.com:

"Does evil.example.com have permission to send this kind of request?"
        ↓
Browser sends: OPTIONS /api/transfer
        ↓
If bank.example.com says "No":

 Browser does not send the actual transfer request

The server might respond with permission:

Access-Control-Allow-Origin: https://evil.example.com

or refuse to provide that permission.

If the server does not allow the attacker's origin, the browser blocks the actual JSON request.

Preflight allowed?

        β”œβ”€β”€ No
        β”‚     ↓
        β”‚   βœ— Actual JSON request is blocked
        β”‚
        └── Yes
              ↓
            Browser may send
            the actual request

This creates a useful barrier against some traditional CSRF attacks.

But JSON is not automatically safe.

The protection comes from several things working together:

JSON request
        +
Browser's cross-origin request rules
        +
Server's CORS policy
        ↓
Cross-site JavaScript may be blocked

The important question is:

Can the attacker make the browser send a request that the server will accept?

Gap 1: The Server Accepts JSON Even When the Content Type Is Wrong

The first thing to test is whether the server really requires:

Content-Type: application/json

A poorly configured server might accept this instead:

Content-Type: text/plain

{"toAccount":"attacker-account-001","amount":5000}

This matters because an HTML form can send text/plain.

For example:

<form action="https://bank.example.com/api/transfer"
      method="POST"
      enctype="text/plain">

  <input name='{"toAccount":"attacker-account-001","amount":5000,"x"
         value='":""}'>
</form>

The resulting body may look similar to:

{"toAccount":"attacker-account-001","amount":5000,"x"="":""}

The exact result depends on the form encoding and server parser, but the security issue is the same: the attacker is trying to send JSON-shaped text through a content type that a form can produce.

The dangerous server behavior looks like this:

Request arrives
        ↓
Server sees: Content-Type: text/plain
        ↓
Server ignores the content type
        ↓
Server tries to parse the body as JSON anyway
        ↓
 Attacker may be able to forge the request

A safer server checks the content type before parsing:

Request arrives
        ↓
Is Content-Type application/json?
        β”œβ”€β”€ No
        β”‚     ↓
        β”‚   Reject request
        β”‚
        └── Yes
              ↓
            Parse JSON

The server should not merely ask:

"Can I parse this body as JSON?"

It should also ask:

"Was this request sent using the content type that this endpoint requires?"

Gap 2: The Server Accepts Parameters From Other Locations

The second thing to test is whether the same action can be performed without a JSON body.

An endpoint may be documented like this:

POST /api/change-email
Content-Type: application/json
{
  "email": "user@example.com"
}

But the application might also accept:

POST /api/change-email?email=attacker@example.com

or:

POST /api/change-email
Content-Type: application/x-www-form-urlencoded

email=attacker@example.com

If so, the attacker does not need to send JSON. The attacker can use an ordinary HTML form instead.

Application expects:

JSON body
        ↓
But the framework also accepts:

Query-string parameters or form parameters
        ↓
Attacker uses a normal form
        ↓
 JSON restriction is bypassed

This can happen when a framework combines parameters from several sources:

JSON body
    +
Query string
    +
Form fields
        ↓
One combined parameter object
        ↓
Same application handler

The security review should therefore ask more than:

"Does this endpoint use JSON?"

Ask:

Can an attacker-controlled website
send any request that this endpoint accepts?
        ↓
Check:

βœ“ Does the server require application/json?
βœ“ Does it reject text/plain?
βœ“ Does it accept form fields?
βœ“ Does it accept query-string parameters?
βœ“ Does it parse the same action from multiple formats?
βœ“ Does CORS allow the attacker's origin?
βœ“ Are cookies sent with the request?
βœ“ Is a CSRF token still required?
4

Explanation

Form Submissions

Form-based CSRF should be tested systematically across every state-changing form the application exposes not only the obviously high-value targets such as password changes or financial actions.

Lower-profile forms can still produce meaningful security impact:

  • Profile updates.

  • Email or notification preferences.

  • Comment and reply submission.

  • Address changes.

  • Subscription or billing changes.

  • Account settings.

  • Content creation or deletion.

  • Administrative actions.

The goal is to identify every place where an attacker might be able to cause the victim's browser to perform an action using the victim's existing authentication state.

Application
        ↓
Enumerate every state-changing form
        ↓

For each form: What request does it send?
        ↓
Can another origin reproduce that request?
        ↓
Would the browser attach the victim's authentication
credentials in that context?
        ↓
Does the server require something the attacker cannot provide?
        ↓
        β”œβ”€β”€ No
        β”‚     ↓
        β”‚   ❌ Potential CSRF
        β”‚
        └── Yes
              ↓
            βœ“ Request rejected

Instead of looking for object identifiers, the focus here is on state-changing operations:

Crawl application
        ↓
Find forms and action endpoints
        ↓
Identify:
POST / PUT / PATCH / DELETE
        +
Any GET request with side effects
        ↓
Inspect every state-changing request
        ↓
Test its CSRF protections

For each form, identify:

1. Target endpoint

2. HTTP method

3. Submitted parameters

4. Authentication mechanism

5. Whether cookies are sent in a cross-site context

6. Whether a CSRF token, Origin check, or another
   server-side validation is required

The practical test is then to reproduce the request from a different origin while deliberately omitting any valid CSRF token or other value that should prove the request originated from the legitimate application.

Victim is logged in
        ↓
Attacker-controlled origin constructs equivalent request
        ↓
No valid CSRF token supplied
        ↓
Browser sends request if the request mechanism
and cookie rules permit it
        ↓
Server response
        β”œβ”€β”€ Request succeeds
        β”‚     ↓
        β”‚   ❌ Potential CSRF vulnerability
        └── Request rejected
              because required validation is missing or invalid
              ↓
            CSRF defense working

CSRF protection is an endpoint-by-endpoint property. An application might correctly protect:

POST /change-password

while completely forgetting protection on:

POST /change-email

POST /notification-settings

POST /subscription/cancel

POST /comments/delete

Any one of those omissions can become the weakest point in the application's overall CSRF posture.

5

Explanation

Password Changes

Password-change endpoints are particularly high-value CSRF targets because a successful forgery can do more than modify an account setting it can potentially lock the legitimate user out of their own account and give the attacker a credential they control.

A vulnerable flow might look like this:

Victim is currently logged in
        ↓
Victim visits attacker-controlled page
        ↓
Attacker causes a forged request:

POST /change-password
newPassword = attacker-chosen-password
        ↓
Victim's browser may attach the existing session cookie
        ↓
Application accepts the request
        ↓
Victim's password is changed
        ↓
  Victim may be locked out

If the attacker knows the password they caused the application to set, they may then be able to authenticate to the account themselves.

Attacker chooses new password
        ↓
CSRF changes victim's password
        ↓
Attacker knows the new credential
        ↓
Attacker attempts normal login
        ↓
  Potential account takeover

This makes password changes significantly more dangerous than many lower-impact CSRF targets.

A particularly vulnerable design allows an already-authenticated session to change the password using only:

newPassword

or:

newPassword
confirmPassword

with no additional verification.

Authenticated session
        ↓
POST /change-password
newPassword = attacker-choice
        ↓
No CSRF validation
        +
No re-authentication check
        ↓
 Password can potentially be changed through a forgery

Requiring the user's current password or another form of fresh authentication adds an additional barrier because an attacker performing a blind CSRF attack typically does not know that value.

However, this should be understood as defense in depth, not a replacement for CSRF protection.

Current-password check
        ↓
Helps prevent an attacker from forging a password change
But:
        ↓
It does not prove the request originated from the legitimate site
        ↓
CSRF protection should still be applied where cookie-based
authentication creates CSRF risk

For sensitive operations such as password changes, a stronger design often combines multiple controls:

Password-change request
        ↓
βœ“ Valid authenticated session
        +
βœ“ Valid CSRF protection
        +
βœ“ Fresh authentication (current password, MFA,
   passkey, or another appropriate re-verification)
        ↓
βœ“ Password change permitted

A successful CSRF attack against an ordinary profile field changes data. A successful CSRF attack against a password-change endpoint can change the credential that controls the entire account.

6

Explanation

Email Changes

Email change endpoints deserve the same dedicated attention as password changes, but for a slightly different reason: changing an account's registered email address can become the first step in a larger account-takeover chain.

A typical chain looks like this:

Attacker causes a forged email-change request
        ↓
Account email changed to: attacker@example.com
        ↓
Attacker initiates: "Forgot password"
        ↓
Password-reset link is sent to the attacker's email address
        ↓
Attacker resets the password
        ↓
  Potential account takeover

The immediate effect of a vulnerable endpoint may appear minor only a single account field has changed but the downstream consequences can be much more serious.

A particularly weak implementation might accept:

POST /change-email

email=attacker@example.com

based solely on the victim's existing authenticated session.

Victim is logged in
        ↓
Victim visits attacker-controlled page
        ↓
Forged request submitted
        ↓
Browser may attach the victim's session cookie
        ↓
Application changes email
        ↓
Account recovery channel may now be attacker-controlled

Re-authentication as Defense in Depth:

Because changing an email address affects account recovery and identity, the operation should generally be treated as a sensitive action.

Requiring fresh authentication for example, the current password, MFA verification, or another appropriate step-up authentication mechanism creates an additional barrier.

An attacker performing a classical blind CSRF attack generally cannot provide the victim's current password or complete a fresh authentication challenge.

However, this is defense in depth, not a substitute for CSRF protection.

Confirming the New Email Address:

Because the attacker controls the new address in a CSRF scenario, sending a confirmation link only to that address does not meaningfully protect the accountβ€”the attacker can complete that verification themselves.

A safer design requires confirmation through the existing email address before replacing it.

User requests:

new-email@example.com
        ↓
Application sends confirmation to the current email address
        ↓
Legitimate user confirms the requested change
        ↓
Account email is updated to:
new-email@example.com

The application may also send a notification or secondary confirmation message to the new address, but control of the new address should not be the only condition for finalizing the change.

This creates an additional barrier against CSRF because an attacker who can cause the request but does not control the victim's existing email account cannot complete the change.

For sensitive applications, it can also be valuable to notify the existing email address when an email-change request is initiated or completed.

A focused CSRF review should check:

βœ“ Can the email-change request be forged
  from another origin?

βœ“ Does the endpoint enforce its intended
  CSRF validation?

βœ“ Is fresh authentication required for
  this sensitive action?

βœ“ Is the new address verified before the
  change becomes fully effective?

βœ“ Is the existing address notified when
  a change is requested or completed?

βœ“ Could control of the new email lead
  directly into a password-reset flow?

You've completed Cross Site Request Forgery

Great work β€” explore other topics to keep learning.