Blog post

Front-End Frameworks: When Bypassing Built-in Sanitization Might Backfire

Stefan Schiller photo

Stefan Schiller

Vulnerability Researcher

7 min read

  • Security

Modern JavaScript front-end frameworks like React, Angular, and Vue.js safeguard your application from Cross-Site Scripting (XSS) vulnerabilities by automatically escaping untrusted content. While this is a suitable and safe solution for most use cases, there might be scenarios where developers want to directly render HTML and thus need to bypass this protection.


This is obviously dangerous, and it’s a developer's responsibility to ensure that the inserted content is safe. For this, it is crucial to verify that a malicious user cannot control the data that is inserted as raw HTML. However, other unrelated issues in the application can quickly falsify the assumption of what can be controlled and what cannot - leading to an XSS vulnerability.


This blog post will showcase the dangers of bypassing a framework’s built-in sanitization by explaining how attackers could have exploited the finance application Firefly III. We will explain how a combination of Client-Side Path Traversal and a deliberate Sanitization Bypass could make your application vulnerable, too.

Bypassing Built-in Sanitization

For the sake of this blog post, we will stick to Vue.js, which is used by Firefly III. The same principles apply to other JavaScript front-end frameworks like React and Angular.


Vue.js uses the Mustache template syntax with double curly braces to interpolate text into an element:

<template>
  <div>{{ userInput }}</div>
</template>

The built-in sanitization ensures that even if userInput contains malicious HTML like <img src=x onerror=alert(1)>, no alert box is triggered since the value of userInput is inserted as text only. This can be verified by inspecting the syntax highlighting in the DOM tree visualizer of the browser devtools. The whole img tag is colored in black:

There might be a use case where a developer does not only want to dynamically insert text but raw HTML. For this purpose, the v-html directive can be used to bypass the text-only limitation:

<template>
  <div v-html="userInput"></div>
</template>

If userInput contains <img src=x onerror=alert(1)> now, it is actually inserted as raw HTML and the alert box is triggered. The syntax highlighting in the DOM tree now looks like this:

This deliberate bypass of the built-in sanitization should be used with caution and only in scenarios where it can be ensured that a user cannot control the value that is inserted as raw HTML. This does not only apply to Vue.js, but also to other JavaScript front-end frameworks.


Sonar’s source code analysis provides more than 400 rules for JavaScript, including specific rules for React, Angular, and Vue.js. When we analyzed the popular finance application Firefly III on SonarCloud, one of these rules was triggered. This issue quickly caught our attention:

View this issue on SonarCloud


In the following section, we explain why this is an unsafe bypass and describe how attackers could leverage Client-Side Path Traversal (CSPT) to control the error_message value that is rendered as raw HTML.

Firefly III Sanitization Bypass & Client-Side Path Traversal (CVE-2024-22075)

When inspecting the file containing the issue raised by SonarCloud, we noticed that the error_message variable is populated in the catch-block of an Axios request made to the /api/v1/webhooks/ endpoint. The catch-block is entered when the web server responds with a non-2xx status code. In that case, error_message is populated with the message value of the JSON response:

downloadWebhook: function (id) {
      axios.get({% mark yellow %}'./api/v1/webhooks/'{% mark %} + {% mark yellow %}id{% mark %}).then(response => {
        // ... handle response ...
      }).catch(error => {
        {% mark yellow %}this.error_message = error.response.data.message;{% mark %}
      });
    },

The id variable passed to the downloadWebhook function is appended to the requested API endpoint. This id is taken from the browser's current URL via the window.location.href attribute:

const page = {% mark yellow %}window.location.href{% mark %}.split('/');
const webhookId = page[page.length - 1];
this.downloadWebhook({% mark yellow %}webhookId{% mark %});

Thus, the request issued by the browser looks like this when the id is 1, for example:

An attacker who would like to inject HTML code into the error_message would need to make the API request return a non-2xx status code and control part of the JSON message value returned from the web server.


Since the id passed to the downloadWebhook function is directly taken from the browser's URL and appended to the requested API endpoint without any sanitization, an attacker can craft a malicious URL with an id that traverses to another API endpoint. This technique is known as Client-Side Path Traversal (CSPT).


Let's consider the following example. Usually, the browser's URL looks like this:

http://example.com/webhooks/edit/{% mark yellow %}1{% mark %}

The id is populated with all content of this URL after the last slash. Thus the id is 1 for this example. The corresponding API request made by the client-side JavaScript code is this:

http://example.com/api/v1/webhooks/{% mark yellow %}1{% mark %}

An attacker can leverage this by crafting a malicious URL like this:

http://example.com/webhooks/edit/{% mark yellow %}1#/..\..\..\some\other\endpoint{% mark %}

The 1# at the beginning is necessary to make the server-side endpoint handler respond with a valid page. If the attacker now tricks an authenticated victim into visiting this link, the victim's browser extracts the id, which is everything after the last forward slash:

{% mark yellow %}..\..\..\some\other\endpoint{% mark %}

This id is appended to the requested API endpoint and the victim's browser normalizes the backslashes to forward slashes. Thus the browser performs a request to the following endpoint:

http://example.com/{% mark yellow %}some/other/endpoint{% mark %}

An attacker can leverage the /reports/default/1/<start>/<end> endpoint to control parts of the returned JSON message value. This endpoint tries to convert the start and end path parameters to DateTime objects. When this conversion fails, it returns an HTTP 500 Internal Server Error response, which reflects the end value in the message response:


Request

GET /reports/default/1/0/{% mark yellow %}INJECT{% mark %} HTTP/1.1
Host: example.com

Response

HTTP/1.1 {% mark yellow %}500 Internal Server Error{% mark %}
Date: Tue, 19 Dec 2023 09:30:45 GMT
Server: Apache
...

{"message":"Internal Firefly III Exception: Failed to parse time string ({% mark yellow %}INJECT{% mark %}) at position 0 (I): The timezone could not be found in the database","exception":"Carbon\\Exceptions\\InvalidFormatException"}

This allows an attacker to use the Client-Side Path Traversal vulnerability to reach the XSS sink:

An attacker can, for example, craft the following malicious link:

http://example.com/webhooks/edit/1#/..\..\..\..\reports\default\1\0\{% mark yellow %}%3Ch1%3EHACKED%3Cbr%3E%3Cbr%3E{% mark %}

If an authenticated victim clicks on this link, and there is a least one webhook configured, the HTML code is injected into the page:

Limited Impact Due to Strong CSP

Fortunately, the default setup of Firefly III employs a strong Content-Security-Policy (CSP) that prevents an attacker from performing Cross-Site Scripting (XSS). The vulnerability could still be used to inject arbitrary HTML or CSS into the page. For example, an attacker can inject a meta tag, which immediately redirects the user to another page. This can be used in a phishing attack to redirect the user to a page that looks similar to the Firefly III application and prompt the user for their credentials. Alternatively, an attacker could leverage CSS data exfiltration techniques or craft a fake UI and trick the user into making a form submission to the application (submitting a form to another origin is prevented via the CSP).

Patch

The vulnerability was fixed with Firefly III version v6.1.1. Since the error message is supposed to be populated with raw HTML, the v-html directive was not removed and two mitigations were applied to prevent an attacker could control this value.


At first, the Client-Side Path Traversal vulnerability was fixed by converting the webhookId extract from the URL to an integer:

-     const webhookId = page[page.length - 1];
+     const webhookId = parseInt(page[page.length - 1]);

Secondly, the error message raised by the /reports/default/ endpoint was changed so that it does not contain any dynamic data and only a static error message:

-       } catch (InvalidDateException $e) { // @phpstan-ignore-line
+       } catch (InvalidDateException|InvalidFormatException $e) { // @phpstan-ignore-line
           $message = sprintf('Could not parse date "%s" for user #%d: %s', $value, auth()->user()->id, $e->getMessage());
           app('log')->error($message);
-           throw new NotFoundHttpException($message, $e);
+           throw new NotFoundHttpException('Could not parse value', $e);
       }

It is generally a good approach to only return static error messages, as highlighted by one of our recent findings in Mailcow, where a controlled error message led to XSS.


If your application uses built-in sanitization bypasses, we recommend reconsidering whether they are really required or cannot be circumvented. If necessary, the data that is inserted as raw HTML should be sanitized beforehand, for example, by using a client-side sanitizer like DOMPurify.

Timeline

DateAction
2023-12-20We report the issue to the Firefly III maintainers.
2023-12-20Firefly III maintainers acknowledge our report and provide a patch.
2023-12-26Fixed version v6.1.1 is released.

Summary

In this blog post, we highlighted the need to take great care when bypassing built-in sanitization in JavaScript front-end frameworks. For use cases where this is really necessary, the data inserted as raw HTML should be sanitized to allow only necessary and safe tags and attributes. The Firefly III vulnerability covered in this blog post showed that this is not always easy.


We demonstrated how attackers might leverage a Client-Side Path Traversal vulnerability to control values that were assumed to be uncontrollable. Because of this, data inserted as raw HTML should be sanitized properly beforehand. Furthermore, a strong CSP should act as an additional defense-in-depth mechanism to reduce the impact of vulnerabilities like this. 


At last, a huge shoutout to James and the rest of the Firefly III team for quickly verifying our report and providing a comprehensive patch. Thank you!

Related Blog Posts

Get new blogs delivered directly to your inbox!

Stay up-to-date with the latest Sonar content. Subscribe now to receive the latest blog articles. 

By submitting this form, you agree to the storing and processing of your personal data as described in the Privacy Policy and Cookie Policy. You can withdraw your consent by unsubscribing at any time.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.