Mass assignment bugs occur when developers allow parameters from HTTP requests to bind directly to objects without any validation. This potentially allows attackers to modify or add parameters that they should not have access to.
This happened when I developed a web app for my personal use. I know I am not a good developer. The reason why I built the app is just to feel the developer's perspective from the code and make it live in production. I want to know every process they’ve been made, and I think this is one of the best ways to learn. If you want to attack a web application, sometimes you need to be the person who made it.
Considering you have a route endpoint for registration, the app asks you to input your first name, last name, and email, it seems normal at first. But after you submit to the registration endpoint and check your profile, the server responds with an additional object key and value.
is_admin object key is hidden when you register, and can only be seen when you are authenticated.
Have you spotted the bugs yet?
Yes, as a pentester of course you will test the input at first, modifying and tampering with the request body, and more importantly the input will be stored in the database. Wow!
Continuing the test, I tried to create a second account to test the register endpoint, I have added the is_admin key with a ‘true’ value to be inserted at the first register, as you guessed. The server responds with an additional value that I inserted before. Confirm this is a valid bug!
I opened the VS Code to check the related code, and immediately I knew the bugs, and now I know what the bugs look like in a real app. Here’s the simplification of the pseudocode.
router.post('/register', auth, async (req, res) => {
const user = new User({
...req.body,
})
try {
await user.save()
res.status(201).send(user)
} catch (error) {
res.status(400).send()
}
})
The /register endpoint accepts the input from the request body using the javascript ES6 spread operator, (denoted by …) This operator is commonly used to copy or merge arrays and objects, in this case, accept all request body (…req.body) from /register endpoint directly without any validation and save them to the database. With that information, you can inject anything to request body and the app will be happy to accept it.
Of course, that is something you don’t want to happen in your production app, the next step is to make sure that the app will perform input validation before accepting the input sent by the user.
router.post('/register', auth, async (req, res) => {
try {
const regData = Object.keys(req.body)
const allowData = ['firstName', 'lastName', 'email']
const isValidData = regData.every((data) => allowData.includes(data))
if (!isValidData) {
throw new Error ('Error!')
}
const user = new User(req.body)
await user.save()
res.status(201).send({})
} catch (error) {
res.status(400).send({})
}
})
Let’s modify the code a bit, before req.body data saved to the database, the application performs validation by using every() and includes() array methods, it will test req.body data match to allowData array, and it will return true or false, then we can use the if function to throw an error if it does not satisfy the condition.
Now any attempt to modify or add data to the request body will get a 400 status Code.
Thanks for reading!