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?