Access control is one of the crucial elements to application security. The vast majority of applications have some data or functionality that’s intended to be limited to a particular user. For example, you can change your own password. In most apps, you’re not allowed to change another user’s password. We would describe that as an authorization boundary between users. Some apps might have the concept of teams, where there’s a permission boundary between teams. Only users on a given team can access certain resources or functions related to that team. In a multi-tenant system, there are going to be boundaries between tenants, where Organization A’s users shouldn’t be able to see Organization B’s stuff. When it comes to looking at actual HTTP request traffic, we can (and should) check that the authorization is being enforced properly. We can leverage ZAP’s Request Editor to help us analyze that behavior.
The classic example of a session is a cookie containing a strong, generated identifier which is included in requests to the server, and that the server then uses to retrieve your session information for some sort of fast temporary storage.
But really, these same checks can be applied as long as there’s a value in the request representing the user’s identity and (directly or indirectly) level of access. This means in addition to session cookies, this applies to OAuth JWTs, SAML 2.0 tokens and API keys and fully-encrypted tokens that you can’t read. These principles remain the same. Signed tokens that the client can read have additional considerations in the next section.
With that in-mind, below is a sample request, followed by the main points I want to prove when evaluating any given operation in the app.
If I find the request where I received the session, and replay it (unmodified), will I get a different value? In this example, it will be a POST containing the username and password, and I’m looking at the Set-Cookie header in the response. Generally I want to see a different session ID each time. If I get the same one back, I’ll want to explore that further to better understand the behavior.
Test this by removing the cookie from the request completely and reissuing it with the Request Editor.
If authorization is being enforced, I should receive a response that is different from the original response. In most cases, this should be a 4xx error or a redirect. In my sample app, it was a 303 redirect, sending the user to /login
It’s unlikely that the app is just checking if the cookie is there, but not doing anything with it. However, I’m hired to do penetration testing, not penetration assuming. I’ll prove the hypothesis by replacing the cookie value with another value that I’m certain isn’t a valid session, as pictured.
In my app, this produced the same 303 response as when the cookie was missing outright. In most apps, this will be the case, because an invalid session and an expired session are often indistinguishable to the server-side process performing the lookup.
There are a few setup steps for this test, but I need to start by identifying a transaction that’s a good candidate for it. Here are the main qualities I’m looking for:
For this example, let’s assume I see this request structure to add a user to a team. The current session showing belongs to a user who is a member of the team and has those permissions.
Once I decide I want to test this request, I would use the browser’s Developer Tools to delete the cookies.
Note that I wouldn’t want to use the Logout functionality in the application, because that should invalidate the session. This will allow me to return to the login and use another account, while leaving the previous session alive until the server cleans it up as part of its session timeout policy.
Once I login with my other account, this one on the other side of the authorization barrier, I can find that request to add a team member, and open it in the Request Editor.
I’ll definitely replace the original session ID with the one belonging to my current user, who is not supposed to have access to team 3. I’ve also changed the userId from 1 to 3 in both the route and the request body, and changed the role to owner.
When I send this request, the correct behavior would be some sort of authorization error or perhaps even a 404. On the other hand, if it returns a 200, and I can verify that user 3 (my current user) has been added to the team that I’m not supposed to have rights on, then the authorization is not correctly enforced. In this case, the auth was broken and the user made himself an owner of that team, as shown in the response below.
There are specific attacks and common implementation flaws for different token types. My examples here will use JWTs and cover a couple of common attacks, but this sort of process is applicable to any token that:
Also, to reiterate, every test in the previous section applies to these as well. Below is a sample request with a JWT bearer token, followed by additional checks to make against these tokens.
While some token types don’t contain it, JWTs specifically do. And it’s useful information to have, in order to inform how you approach the other parts. If it’s your app, you may just know it. However, you may just want to decode the JWT and verify it.
Highlight the first section of the token (up to the first period character), right-click, and select Encode/Decode/Hash… from the context menu.
This will open a dialog with tabs for different encodings. Choose the Decode tab, and read the alg claim from the Base64 Decode section.
My example is HS256, meaning the signature is a message digest for the rest of the token, hashed with a secret.
If it was a signing algorithm based on asymmetric encryption, such as RS256, that would be worth noting. Either one can be done securely, but there are minor differences in the most efficient approaches to attacking them.
If we use ZAP’s encoding/decoding facilities, we can easily modify the token. An easy change for this one is to copy the decoded value for the whole JWT head, from where I grabbed the alg, and paste it into the Text to be encoded… box at the top of the dialog.
I still want it to be well-formed, but I can make a benign addition to the header’s claim, and then copy the Base64-encoded value, ignoring any trailing equals (=) signs (they’re padding characters that don’t belong in .
Now, if I use that value to replace the header in the Request Editor, and replay the request, I should expect to see a signature verification error.
In my app, I got a 401 error with a message indicating an invalid token signature, as expected. This means that my app saw that the payload didn’t match the signature, therefore it had been tampered with and should be considered invalid.
If we pretend that I had received a regular 200 response, with the tampered token, my next step would be to grab the middle section of the token (between the first and second period characters), and similarly decode and modify that. This section varies greatly from one app to another, but as you can see in the following screenshot, this example has the userId and username claims.
If the server isn’t validating the signature, I can freely change these to another use with different rights. Another application may have claims for tenant ID, or user group memberships, or a number of other sensitive fields.
Following the same steps as previously mentioned to construct a modified header, we can get a variant of it with a signing alg of none.
Much like last time, I can now go into the Request Editor and replace the header section. The only difference is that this time I’m also removing the signature (leaving the second period).
Most applications will have the none alg disabled, so this will not work. However, occasionally this is enabled either by default or by mistake, and allows the attacker to modify the claims at will and effectively bypass the signature check.
There are other flaws that JWTs can have, such as the Key Confusion attack if a public key is available. Or if HS256 is used and the secret is weak, guessable or in wordlists, an attacker able to determine it can sign their own modified tokens. Other token types such as SAML 2.0, can have their own implementation flaws. To reiterate, it’s really a topic unto itself, but this is at least an intro to some of the attacks to try.
Check out Day 4 for a rundown of fuzzing to find injection flaws.