The Dynamic Duo of Broken Access Control
There's a reason that Broken Access Control sits at the top of the OWASP Top 10.
When developers get access control wrong, the consequences are immediate and severe. Users can view data that isn't theirs, perform actions outside their role, or even escalate privileges to become administrators.
These aren't just theoretical risks.
They're the kinds of issues that consistently show up in penetration test reports time and time again.
Unlike obscure vulnerabilities buried deep in cryptography, access control flaws are often simple oversights that slip past code reviews and automated scanners, yet they lead directly to data breaches, reputational damage and becoming the next headline on the news.
Not a place any company wants to be in.
Now if you are unfamiliar with either Mass Assignment or Insecure Direct Object Reference (IDOR), prepare for a 101 in both classic vulnerabilities.
Mass Assignment
In short, mass assignment is when a client can add additional information in a request that is accepted by the server.
Not so nefarious on the surface, but what if that information is controlling the variable which determines if the user is an administrator or the owner of a tenancy.
Picture this.
Your application allows user to edit their profile.
The request looks something like:
POST /api/users
{
"username": "alice",
"email": "alice@example.com"
}
And it just so happens the backend contains a variable to identify super users amongst the pack.
Which leads us to exhibit A.
POST /api/users
{
"username": "alice",
"email": "alice@example.com",
"is_admin": true
}
And now Alice is an admin…
Some might say that example is overly simplistic, and they would be right.
But the sad truth is that it is more common than you think.
Now that we know how mass assignment is exploited, we can look at how we can prevent it.
Most frameworks have a method for either allowlisting or blocklisting, and when it comes down to it, allowlisting prevails.
Looking at an example Cursor has graciously provided for us in Django, we have a profile of a user that is currently listed as an employee.
With some good guess work they submit the request.
curl -X POST https://vanguardcyber.lab/employee/3/update/ -H "Content-Type: application/json" -d '{"is_admin": true}'
Which unfortunately works and updates their role to an administrator, giving them access they didn't previously have.
Now they are able to access the Employees panel and found themselves with control over the application.
Now that might sound farfetched to exist in a modern web application.
Sadly, we see it time after time.
It's a privilege escalation here or a lateral movement there that can expose your entire web application.
You're probably thinking that is all well and good, but how do you fix the issue.
Well, when we look at the code, which in this instance we'll use Django as our test subject.
The request to the server that is responsible for updating user profiles, seems to blindly accept user input and apply it.
The code is only checking if the user "hasattr" or has the attribute, then it goes ahead and sets the attribute with "setattr".
Now it's obviously clear that something is wrong with the code, and we shouldn't be automatically updating a user based on what they provide.
Previously, we said that allowlisting was generally preferred to applying a blocklist.
And to solve our issue we can do just that.
We create a list of all the user fields that we want to allow the user to modify.
In this instance, we've called it SAFE_USER_FIELDS.
Once in place, users will only be able to update the fields that you want them to, not the other way around.
So that covers off how you can avoid mass assignment ruining your Friday afternoon.
Let's look at what happens when IDOR vulnerabilities go overlooked and start to plague a web application.
Insecure Direct Object Reference
IDOR allows a user to access resources like profiles and pages through modifying the reference to a page.
To break it down, a classic example is a URL that looks something like:
https://vanguardcyber.lab/employee/2/
or
https://vanguardcyber.lab/employee/550e8400-e29b-41d4-a716-446655440000/
Where the 2 and 550e8400-e29b-41d4-a716-446655440000 in this case will be the identifier for the user.
As much as it is harder to guess a UUID or Universally Unique Identifier, the addition of obscurity isn't the best defence against IDOR.
Protection comes when the user is validated prior to accessing a resource on the server.
For our test case though, we have made it bleedingly simple to see how if your application has IDOR a user will be able to have more access than you expected.
For us, we have John.
He's logged in and looking at his profile page.
But it turns out he's curious and noted that his URL says, https://vanguardcyber.lab/employee/2/ and if he's number 2, who is number 1.
As there is no actual intentional functionality in the application that allows users to view other user's profiles, he's decided to modify the URL to be:
https://vanguardcyber.lab/employee/1/
And voila!
Now that's not John's profile.
Instead, he's found himself the administrator of the site, with their username and details.
What he was meant to see was a clear message that he will curb his curiosity for now.
A clear message that he will curb his curiosity for now.
So, what caused this and how did it get fixed.
Looking at the code there is the line in the view_profile function that will render the user's profile without first checking that the user requesting the page is authorised to see it.
A simple fix is to do just that.
Once there is a check in-place, it will look like this.
So now, we have a redirect in place for any user that isn't looking for their own profile.
if not request.user.is_admin and request.user.id != user_id
The quick conditional check will ensure that they aren't an admin and the request is sent from the correct user.
Done.
A couple of quick code changes can prevent a major issue popping up in your application.
But of course, we haven't covered nearly enough of the broken access control issues to say that's all you need.
Yet for now it is a good start.