Discovering and Exploiting API Attack Surface Using Client-Side Javascript

Jul 5 · 10 min read

Have you ever been on an engagement where the client, while providing you with the information you needed to start, didn’t give you the whole picture? You know those bodies in the digital backyard they hoped you don’t find, but somehow always seem to turn up. That happened to me recently on an web app test. We were provided a short engagement window and a lot of application to grey box test. As part of SOP we asked for any API swagger/WSDL information that would help us test and confirm access controls. We got information on a bunch of third party APIs, but the customer said they weren’t using any they had developed. OK, you ask for nothing what do you get?

As it turned out, once I began crawling the site, I noticed all of these calls to a REST API that we were told didn’t exist. I knew what it was instantly because of the URL

What have we here? Is this a custom API?

I started watching for that particular URL as I moved around the site. I saw a few more endpoints pop up, and I watched the data being sent/received to get a better handle on this thing. I didn’t know it at the time but I would later discover that it was the gears of the entire application. Maybe something that should have been disclosed at the beginning. Usually when someone omits something as big as this, they are already well aware of the issues that plague the undisclosed API. It’s also a big indicator of what to focus on. I figured that I could map this sucker out no problem. Well, a day went by and I got a whopping five very uninteresting endpoints. This API couldn’t have only five endpoints. Then I realize maybe this particular (read-only access) user role only has (legitimate) access to those five endpoints. Great… Now what?! First I needed to figure out how the access controls were implemented.

On one page the application presented dead buttons that teased me with the hope of adding various content to the site, but when I clicked, a tooltip opened telling me that I needed to contact someone to request more access. The culprit was some Javascript calling preventDefault() and then showing a modal instead. I cracked open the dev tools, inspected the button element and chased down the the piece of Javascript that was attached to the event listener for that button. It looked as though the button’s click event listener was determined by the CSS style class it was given on load.

read-only style class dictates button behavior

Once I removed it, the button would preform its default action… Which was nothing. Yeah, not the action I was hoping for, but still getting warmer. I could try one of two things here. I could hunt down a more appropriate class to assign the button by looking in the stylesheet files (there were 8 stylesheets not counting inline and third party), but ain’t nobody got time for that.

Scraping Javascript

Using the JSTool plugin to pretty print a Javascript file

Dev tools in the browser can also pretty print the Javascript, but the searching capabilities are a bit more robust in Notepad++. Next I searched for the keyword “index.php/admin/rest”. I got a hit… Then another, and another. It appeared that all endpoints for creating, saving, deleting and modifying a form within the application were defined within this one file. That explained why it’s so large even though the user I’m rocking is a read-only user.

saveData function syntax, endpoint and expected parameter information

Now that I knew what to look for, I could go through all the Javascript looking for the same “index.php/foo/rest/” string. Within 20 minutes, I had acquired not only all API endpoints, but I also knew what parameters and request methods each of them accepted. I laid them all out in front of me and noticed something odd. One of the endpoints I had first mapped had additional functionality. The endpoint in question was at “/rest/userAdmin/setUserData”. Originally, I mapped this endpoint as part of the user profile section of the app. I had been able to use it to change my name, email, password, etc. Pretty boring.

Post request sent to the setUserData endpoint changing the information for the current user

Abusing Permissive API Access Controls

HTML source providing parameters for successful setUserData

One of the pages had a listing of all the admin users. Information listed for each user included: name, email, access role, phone number and some other application specific attributes that I didn’t really care about. I took a look at my user information in this area. I could see my role, adminId, email, etc. I already knew that there are a couple more parameters that can be passed to this endpoint from changing my user information in the settings page, “password” and “password2”. Those attributes didn’t seem to be available on this page though. While I’m poking around at the source, I also take a look at what the other roles were called. I was informed that there were three access roles in this app during the scoping phase: view only, editor, and admin. I was not surprised to find that the application contained a fourth role called “super user”. If that’s something that was omitted, then I wanna be that role. That’s got to have all the cool stuff this app can do available to it.

I sent one of the API requests in burp to repeater and took a look at the structure. It was a POST request to the endpoint “index.php/foo/rest/userAdmin/setUserData”. The parameters in the request were limited to only the boring things. I wondered what happened if I added in those additional attributes that were available in the admin user listing. things like role, or job title. Job title didn’t seem to affect access so role was the parameter to play with. At first I left role blank. Response 200. JSON in the body {‘data’:[]}. I guess that meant it was accepted. I then reviewed the page that lists all the users and lo and behold my assigned roles had become “unknown”. Not a step in the right direction access wise, but it was still a step forward in getting myself the desired access. Next I tried admin. Again the role I had listed had changed. The user account had been assigned an admin role and all the modals were gone when I would click on buttons. Finally I wanted to try “super user”. 500 Server Error. Huh, maybe it doesn’t like the space. I tried to encode that space a bunch of different ways, I even used an underscore to denote the space and also just remove the space. No joy. It wasn’t liking anything I was feeding it. Most of the time I got an unknown role assignment. When I had a space I get a 500 error. Just for giggles I tried a number for the role. I start with 0. Unknown role. 1, Viewer. 2, Editor. 3, Admin. 4, super user. I was now a super user.

Abusing API to elevate user privileges

So this is neat and all, but if I do bad things as this user, the logs will point to me being the mischief maker. I need to be able to be someone else. Now I remembered seeing in the Javascript the ability to call the setUserData endpoint and include adminId= in the url. Taking a closer look, this endpoint is only called in this fashion when one admin is changing the information of another admin. However, it looks like the API endpoint does not use things like password and password2. At least according to the Javascript, you aren’t allowed to change the password there, but you can send a password reset request. This presents us with two paths to account hijacking. One is that we (as the view-only user) change the email address of the target account and send a password reset. The second option is to try changing the password of the account using the API even though the Javascript doesn’t specifically include that functionality.

Just because it’s not used, doesn’t mean it’s not an acceptable and valid parameter to send with the request. I spent a solid 10 minutes playing with the API, I discover that any parameter that is a valid name, is accepted by the server. That means that if I want to change the password for an account that isn’t mine, I just need to fill out the information like I’m changing my account settings, but when sending include the ?adminId= parameter to the url prior to sending. Testing the theory, I instinctively use adminId=1 since the first user is usually the most permissive simply by virtue of being the first and sometimes only user in a database.

Replay of setUserData endpoint from account profile page but adminID=1 added to end of URL

The response appears to indicate a successful request. Now the only thing left to do is login as the admin to verify that the information was changed. Sure enough, when entering in the new credentials I had just set for that user, I was able to login as admin 1, which was coincidentally the name of the user that the tooltip had earlier told me to contact for specific access rights. that’s not needed now though.


  • Remove unused Javascript functions from js files. — disclosure of unauthorized API endpoints
    I’m not sure how many people actually take the time to sift through them on an engagement, but I’ve found huge amounts of data and hidden functionality within these files. This may be a little bit of work up front, but in the long run, you won’t have as much potential for abuse from unused or obsolete functions and there will be less code to load which will allow the application to load faster.
  • Do not rely on client-side mechanisms for access controls. — Improper access controls via client-side scripts
    It may look slick and have the appearance of smooth load time, but if you are placing all the access control decision making on your client-side Javascript, it will eventually fail you. Everything on the client can be modified by the user and therefore cannot be implicitly trusted by the application.
  • API endpoints should have a single purpose. — Complicated API endpoints murky the Access Control waters
    In this instance, we were able to use the setUserData endpoint to not only change our own account information, but to also change other user account information. While this is technically the same function of changing user profile information, it could be argued that changing your settings on your profile is a different function than changing the information of another user. In an ideal situation, the personal profile setting changes should be handled by a different endpoint with only the ability to change the information of the user that is currently logged in. As well, the ability to change ones access role should be removed from this endpoint. Likely in this case, the individual who put this together was attempting to keep a DRY (Don’t Repeat Yourself) format which would make sense to keep it all in one endpoint since they do have similar functions. However, without the additional access controls, we see that the endpoint is exposed to abuse potential.
  • Don’t try to hide things from the people doing the testing.
    You can try, but it usually doesn’t do any good. What many don’t realize is that when I’m testing an application, I’m spending maybe 10–15% of my time actually looking at the browser. The rest of the time I’m watching all those XMLHttp Requests, every single request to every single page, image and 3rd party script source. If it’s used by the application, we will eventually see it. The problem here is that instead of having all the information up front on the mechanics of an application, I’m spending time that could have been saved on mapping out the functionality that was omitted, hidden, or forgotten about. Also, when I see a REST API that I was told didn’t exist, that is where I’m going to focus my attention. If I wasn’t supposed to find this thing, then there’s likely a reason for it.

Thanks to Chris Merkel.


Written by


I’m the last person you should be taking advice from