Blog post

Never Underestimate CSRF: Why Origin Reflection is a Bad Idea

Paul Gerste photo

Paul Gerste

Vulnerability Researcher

Date

  • Security

Whistle is a popular HTTP debugging proxy with over 14k stars on GitHub. It helps users debug HTTP(S) requests on their system and comes with a wide range of match-and-modify features. Its capabilities can even be extended using a plugin system.


In our continuous effort to help secure open-source projects and improve our static code security analysis, we regularly scan open-source projects via SonarQube Cloud and evaluate the findings. In fact, everybody can also do it – SonarQube Cloud is a free code analysis product for open-source projects, regardless of their size or language.


While scanning Whistle's code base, SonarQube reported a CORS misconfiguration issue that turned out to be a serious real-world vulnerability. In this blog post, we will explain the technical details behind the issue, how it can lead to a full system compromise, and how to avoid such bugs in your code.


Impact

We reported the issue to the Whistle maintainer in June 2024, along with patch suggestions. After they developed an initial fix, we discovered the issue was broader than we first thought. Unfortunately, the vulnerability was never fully fixed, and we stopped hearing back from the maintainer. Therefore, the latest version of Whistle (2.9.90 at the time of writing this blog post) is still vulnerable. Since our 90-day responsible disclosure deadline has elapsed, we release this information to allow users to protect themselves.


The vulnerability (CVE-2024-55500) is a Cross-Site Request Forgery (CSRF) issue caused by a CORS misconfiguration. To exploit the vulnerability, an attacker has to trick a victim into visiting a malicious webpage. Once the victim visits, the site can exploit the Whistle instance on the victim's machine without the user noticing it. As a result, the attacker can execute arbitrary system commands on the victim's machine:


Technical Details

Whistle aims to help developers and other technical users debug HTTP requests made in their systems. It can help develop and debug complex applications or trouble-shooting proprietary programs.


To enable debugging of all HTTP requests made on a system, Whistle installs itself as an intercepting proxy. To support HTTPS interception, it also registers a custom certificate authority (CA) that can sign certificates on the fly. After installing itself, Whistle lists all requests in a web interface:



Users can create rules for automatic request modifications, such as redirects, changing request headers, or modifying response bodies. For this, the user must create rules that match URL patterns. These rules can use values that the user creates, for example, to replace a response body with a static value.


The Bug: Origin Reflection

While investigating issues raised in Whistle's code by SonarQube, we came across the following security hotspot. Such security hotspots highlight parts of your code that are security-sensitive and require a thorough human review:



If you want to follow along and explore the vulnerable code section, you can check out the issue on SonarQube Cloud.


What is highlighted here is a potential Cross-Origin Resource Sharing (CORS) configuration issue. CORS can be used by a webserver to allow other websites to interact with it. This is done by setting special Access-Control-* HTTP response headers that the browser will abide by.


In the highlighted code snippet, we can see that the request's Origin header is reflected in the response's Access-Control-Allow-Origin header. This is unsafe because it essentially enables CORS for all origins, allowing any website to send requests to the server and read the response!


If sensitive information is contained in any response, malicious websites can read it when the user visits them. Similarly, the malicious website can also control what data is included in the request, resulting in a Cross-Site Request Forgery (CSRF) vulnerability. This affects Whistle's /cgi-bin/* API endpoints, which can now be called from any website. In addition, the Access-Control-Allow-Credentials header is set to true, causing the browser to include cookies or basic authentication information in these cross-site requests.


The impact of such a vulnerability depends on the actions that an attacker can trigger by forging cross-site requests. As we will see next, the implications are critical in the context of Whistle. CORS controls not only the origins but also the data that can be contained in requests. More specifically, the Access-Control-Allow-Headers response header tells the browser which headers can be sent in the cross-origin request.


At first glance, it looked like Whistle's API only used JSON request bodies. This would require the attacker page to set the Content-Type: application/json header so that the Express.js-based server correctly parses the request. However, it turned out that the server always tries to parse both URL-encoded form data and JSON requests and uses whichever works:

biz/webui/lib/index.js:

app.all('/cgi-bin/*', function(req, res, next) {
  req.isUploadReq = UPLOAD_URLS.indexOf(req.path) !== -1;
  return req.isUploadReq ? uploadUrlencodedParser(req, res, next) : urlencodedParser(req, res, next);
}, function(req, res, next) {
  return req.isUploadReq ? uploadJsonParser(req, res, next) : jsonParser(req, res, next);
}, cgiHandler);


To send a form-encoded body, the attacker would need to set a Content-Type header, this time with the value of application/x-www-form-urlencoded. But shouldn't this also fail since Content-Type is not in the server's Access-Control-Allow-Headers?


In this case, the attacker is lucky because of the Simple Requests concept. Simple Requests can be sent cross-origin without the need for CORS. Similarly, parts of Simple Requests, such as safelisted request headers, can be included in cross-origin requests, even when CORS did not specifically negotiate them. This includes the Content-Type header when its value is either application/x-www-form-urlencoded, multipart/form-data, or text/plain.


This means the attacker can send a CORS request with Content-Type: application/x-www-form-urlencoded even though the server does not explicitly allow the Content-Type header! Attackers can now send arbitrary request bodies to any /cgi-bin/* handler and interact with potentially sensitive features of Whistle.


We reported this to the maintainer, and they fixed the origin reflection behavior by only sending the CORS headers when the request comes from an allowed origin. However, while reviewing the patch, we noticed that attackers can exploit this without using CORS at all!


We already talked about the Content-Type header in Simple Requests, but what else does a request need to be considered simple? There are several minor requirements, but the important one for our case is that a Simple Request can only use the GET, POST, or HEAD methods, which work with most of Whistle's API endpoints.


This shows that an attacker can craft a Simple Request that is still processed by Whistle! We also reported this to the maintainer with suggestions on how to fix it. Unfortunately, they stopped communicating with us since then, and the non-CORS variant has never been fixed. As of the time of writing this blog post, it can still be exploited using a Simple Request.


Exploring the Impact

Let's try to understand this vulnerability's impact on Whistle and its users. As mentioned earlier, the attacker can talk to any /cgi-bin/* endpoint and control the request body. There are various reachable handlers, but two of them stand out.


The /cgi-bin/rules/select endpoint can enable new interception rules. This would allow an attacker to modify requests and responses for specific URLs. The /cgi-bin/values/add endpoint can be used to add new values that can be used in rules, such as alternate response bodies. However, there is a feature that is even more interesting from an attacker's point of view. Whistle enables users to create dynamic rules by providing a scripting interface:



These scripts are executed for each request or response that matches the rule's pattern. During execution, they are isolated from Whistle's main environment using Node.js's vm module. According to its documentation, this module is not considered a security boundary, making it easy for an attacker to break out and execute arbitrary code on the system.


In the case of Whistle, this is done by getting access to the global scope outside of the isolated environment (called "isolate" from here). Whistle passes some objects into the isolate's global context when running the script, such as a values object. Since this object was created outside of the isolate, it also references other things outside of the isolate.


By accessing values.constructor.constructor, an attacker can get a reference to the function constructor of the outside scope. Calling this constructor inside the isolate allows an attacker to create new functions as if they were declared in the host's scope. Such functions can then retrieve useful globals such as process:


values.constructor.constructor('return process')()


This expression creates and calls a function that returns the process object of the host scope. From there, the attacker can get access to the require function via process.mainModule.require and use it to load arbitrary modules, e.g., to execute commands:


process.mainModule.require('child_process').execSync('calc')


These steps showed that an attacker could add a malicious payload using the /cgi-bin/values/add endpoint and then reference it when creating a rule that executes it as a request script for a specific domain. The final attack flow looks like this:



Patch

As mentioned earlier, the vulnerability is only partially patched, and the latest version of Whistle is still vulnerable (as of this blog post). The maintainer's initial fix successfully prevented the origin reflection:


if (checkAllowOrigin(req)) {
  res.setHeader('access-control-allow-origin', req.headers.origin);
  res.setHeader('access-control-allow-credentials', true);
}


Technically, this still reflects the request's origin, but it is limited to trusted origins. However, as we learned earlier, Simple Requests can still be used to communicate with Whistle's API because they don't require CORS. To fix this issue, we suggested checking the Sec-Fetch-Site header to identify cross-origin requests and block them:


if (req.headers['sec-fetch-site'] !== 'same-origin') {
  return res.status(403).end('Forbidden');
}


This header is a so-called forbidden header, which is set by the browser and cannot be changed by the page. It will only have the value same-origin when the requesting page is of the same origin as the destination URL, making it a good solution for this problem.


Timeline

DateAction
2024-06-20We report all issues to the Whistle maintainer via email
2024-08-21We reach out again via a GitHub issue
2024-08-23The maintainer confirms the issues
2024-09-02The maintainer publishes a patch and asks for a review
2024-09-05We follow up after noticing that the issue is still exploitable
2024-11-21Our 90-day responsible disclosure deadline elapses
2024-12-10This blog post is published


Summary

In this blog post, we demonstrated why it is essential to investigate security hotspots raised by SonarQube. An issue that didn't look very impactful at first turned out to be a dangerous vulnerability that can compromise a user's machine just by visiting a malicious website.


We also learned details about CORS, Simple Requests, and forbidden headers such as Sec-Fetch-Site. We hope this equips you with the right ideas for tackling similar issues in your own code. If you need more information, you can visit the Sonar Rules page covering CORS.


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.