The Secret Life of APIs: Uncovering Hidden Endpoints and More
Posted by: Navin Dhas
In the rapidly advancing world of web applications, single-page applications (SPAs) have become a staple for delivering a streamlined and efficient user experience. These applications leverage the power of JavaScript to manage complex functionality seamlessly. A key component of SPAs is their reliance on Application Programming Interfaces (APIs) to perform crucial CRUD operations–Create, Read, Update, and Delete. Given the nature of data handled, which may include sensitive information like Personally Identifiable Information (PII) and Protected Health Information (PHI), ensuring security against potential attacks is paramount.
During an Application Security Assessment (ASA), doing some extra reconnaissance often uncovers hidden or unknown API endpoints in JavaScript files used by an application. This valuable information can then be leveraged to assist with the exploitation of other common application security vulnerabilities, making your assessment more comprehensive and effective.
In this post, we will explore different approaches to discovering useful content in JavaScript, and we will demonstrate some real-world examples discovered during engagements. We will also showcase some helpful tools you can use to validate your applications.
Behind the Scenes: Manually Uncovering Hidden Information in JavaScript Code
APIs play a vital role in SPAs, but what happens when these APIs request, store, or modify sensitive information?
When starting any application security assessment, the first main objective is to “map” the application. This is also referenced in the OWASP Web Application Security Testing Guide. Starting with the highest privileged authenticated user (if applicable), a tester will log into the application and browse the application and use all privileged functionalities available to them.
This step is especially important for SPA applications, as it downloads or builds out static JavaScript files and packages used by the web application. An example site map from Burp Suite can be seen in the following screenshot using OWASP’s intentionally vulnerable web application “Juice Shop.”
Inspecting the “main.js” file in Burp Suite and using a search for the keyword “Administration” finds nine matches in the JavaScript code. In this case it gives us an entry point for an administrator application located under the path ‘/administration’.
Although Burp Suite is the standard for most application security testers, this information can be just as easily discovered using built-in browser tools as seen in the following screenshot of Chrome’s developer tools.
In this scenario, the discovery of this administration page resulted in a “403 – Forbidden” server status message when accessed, but still demonstrates what could be hiding in plain sight in JavaScript files.
From Manual to Automated: Accelerating Discovery with Burp Suite
Although it’s possible to find hidden endpoints and other information manually using Burp Suite and browser DevTools, the tedious process of using “Ctrl-F” to search for keywords such as “admin” and “password” is both inefficient and error-prone. Thankfully PortSwigger and other open-source contributors have provided extensions and other built-in features that automate these common searches. While these tools do not providea comprehensive list of search terms, they do provide some consistency. We have found the following Burp Suite extension to be helpful in finding potentially sensitiveinformation in JavaScript files during engagements.
JS Miner has a plethora of features to inspect and scan JavaScript files, the “API Endpoints Finder” passive scan is a quick go-to for discovery of unknown endpoints. The following screenshot demonstrates some of the endpoints discovered in the “main” JavaScript file for the Juice Shop application.
Another helpful feature in Burp Suite is BChecks, which are additional, custom scanning routines that you can build or import. BChecks are run in addition to Burp Suite’s built-in scanning routines.
While API endpoint discovery in JavaScript is usually successful, sometimes it’s just easier to access the API documentation that might have been “accidently” left available. One such BCheck that discovers this type of documentation is called “Swagger endpoint found.” This BCheck iterates through multiple known paths for web service documentation.
The following screenshot demonstrates the BCheck being used against the Juice Shop application and successfully discovering the “swagger.json” file.
Discovery in Action: Real-World Examples and Insights
Using the techniques and tools discussed earlier, we’ll showcase practical examples of identifying vulnerabilities from information discovered in JavaScript files. These examples illustrate how applying different discovery techniques can help uncover common OWASP Top 10 vulnerabilities like Identification and Authentication Failures, and Broken Access Controls.
Low-Priveleged, Big Impact: Discovering Admin Endpoints
The following real-world example is from an engagement where only a lower privileged test account was provisioned for testing. But with the right reconnaissance, GuidePoint Security was able to confirm the existence of higher privileged roles in the application ecosystem. This was accomplished by reviewing the JavaScript code and discovering the existence of an “/admin” API endpoint. This endpoint would not have been discoverable by other means since no requests would be in the proxy history when using the application as a lower privileged user.
Response
HTTP/2 200 OK
Content-Type: application/javascript
Last-Modified: Wed, 27 Sep 2023 20:28:36 GMT
Accept-Ranges: bytes
Etag: "0b2a3181f1d91:0"
Node: 1
Strict-Transport-Security: max-age=31536000; includeSubDomains
Date: Mon, 20 Nov 2023 18:29:49 GMT
Content-Length: 2597
(function () {
var app = angular.module("TotallyRealWebApp");
var UserAdminController = function ($scope, $http, $filter, accessToken) {
$scope.showExternal = true;
$scope.showInactive = false;
$scope.showInternal = false;
$scope.findFilter = '';
..SNIPPED..
var getUsers = function () {
$http.get(
baseAJAXUrl + 'api/admin/users' + "?access_token=" + accessToken
).success(function (data) {
$scope.users = orderBy(data, $scope.pred, $scope.rev);
..SNIPPED..
A high-level overview of the discovered JavaScript:
- Used to make a GET request to an API endpoint, in this case the “/api/admin/uers” which returns user information a lower privileged user should not have access to.
- “?access_token=” + accessToken” → Adds a query parameter to the URL called access_token from the variable set.
Looking at this code, you might believe the request would fail and no response would be returned. Since our access /bearer token does not have a “Scope” set for an Administrator user, and other observed API requests do not pass the access token in the URL parameters, it would seem like this might be the case.
But what tester could just breeze by this and not give it a try? The following request/response demonstrates how to obtain appalication user data, including passwords of other users, in clear text over HTTP. This is accomplished by modifying a previously used authenticated request of a lower-privileged user in Burp Suite to point to the discovered API endpoint.
Request
GET /api/admin/users HTTP/2
Host: [redacted]
Cookie: [redacted]
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json; charset=utf-8
Authorization: Bearer [redacted]
X-Requested-With: XMLHttpRequest
Content-Length: 0
Origin: https://[redacted]
Referer: https://[redacted]
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Response
HTTP/2 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,OPTIONS,DELETE,PUT
Access-Control-Allow-Headers: content-type, accept, authorization
Access-Control-Max-Age: 10
X-Aspnet-Version: 4.0.30319
Node: 2
Strict-Transport-Security: max-age=31536000; includeSubDomains
Date: Mon, 20 Nov 2023 18:40:45 GMT
Content-Length: 91112
[
{
"$id": "1",
"CUST_NAME": null,
"USERNAME": "[redacted]",
"INTERNAL_USER": -1,
"PASSWORD": "[redcacted]",
"PERSON_LAST": "[redacted]",
"PERSON_FIRST": "[redacted]",
..SNIPPED..
Local Insights: Discovering Authentication Hints in JavaScript Code
On a separate engagement performed by GuidePoint, the application supporting an API appeared to be secured and performing the proper authorization checks on requests. Investigating the “main.js” JavaScript file of this application revealed an interesting tidbit of information regarding authentication headers used in local environments.
if (user && !request.headers.has('Authorization')) {
if (request.url.startsWith('http://localhost')) {
const apikey = _environments_environment__WEBPACK_IMPORTED_MODULE_2__.environment.API_V1_LOCAL_API_KEY;
const userId = user.userInfo.externalId;
headers['X-API-KEY'] = `${apikey}`;
headers['X-USER-ID'] = `${userId}`;
}
}
What immediately sticks out from the code is that, if the request URL is from a localhost and no authorization header is observed, two custom headers are set. While the X-API-KEY value was not disclosed in other JavaScript or used in API requests from the server, the X-USER-ID value was seen in other API responses for the current authenticated user.
Since we could not bypass authorization using other techniques, we decided to modify the request header from the server. The following request/response demonstrates a modified API request with the “X-User-ID” successfully returning information of another user of the application, even though the request does not originate from localhost.
Request
GET /api/v1/user HTTP/2
Host: [REDACTED]
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-User-Id: ocawUmxglD
Authorization: [REDACTED]
Origin: https://[REDACTED]
Referer: https://[REDACTED]
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Te: trailers
Response
HTTP/2 200 OK
Date: Tue, 11 Feb 2025 21:00:43 GMT
Content-Type: application/json
Content-Length: 694
Vary: origin,access-control-request-method,access-control-request-headers,accept-encoding
[REDACTED]: 579
X-Request-Id: 73833725-68ba-4b1e-b9d8-05d0374b405e
X-Content-Type-Options: nosniff
X-Xss-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: *
[REDACTED]
{
"externalId": "ocawUmxglD",
"internalId": [REDACTED],
..SNIPPED..
}
The following screenshot shows a snipped version of the decoded JWT value used in the Authorization header of the preceding API request. Note the “external_user_Id” value is not the same as provided in the “X-User-ID” request header, or the value returned in the API response.
In this post, we’ve explored the often-overlooked world of JavaScript code in search of hidden gems. By combining manual searching, along with automation using Burp Suite, we can uncover valuable information that might have gone unnoticed otherwise. Remember, the next time you’re digging through some JavaScript files, keep an eye out for those hidden admin endpoints and authentication headers – they might just be waiting to be exploited. Happy hunting!
Learn more about GuidePoint Application Security Services