3 minute read

I am currently developing a REST API in Node.js, for learning purposes. It is a simple Tasks API where a user can manage the tasks they need to do and mark them as completed once they finish. I am focusing a lot on the security side of the API, trying to implement consistent authentication and authorization for the application. In this context, I found an IDOR in one of the API endpoints.

The resources are modeled like this: there are three endpoints, Users, Boards, and Tasks, where each resource is managed. Tasks are associated with a Board, and a Board has one owner (the idea is to allow multiple people (like a team) to share a board in the future). Each user authenticates with an email and password, and a JWT token is then issued to authenticate the user in further requests. This token is sent in every request to endpoints that require authorization. Inside the token is the user ID, that is used to check the user identity.

To clarify, a Board creation would be done by making a POST request to /boards:

POST /boards HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIXVCJ9TJV...r7E20RMHrHDcEfxjoYZgeFONFh7HgQ 
...

{"name": "My Board", "description": "Some description"}

And this is how the Board model is defined:

const boardSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    description: {
        type: String
    },
    owner: {
        type: mongoose.Schema.Types.ObjectId,
        required: true,
        ref: 'User'
    }
});

(Ignore the fact that the owner is used as a reference here. The idea against nesting the board inside the user is that I wanted to be able to later implement boards that could be shared between multiple people)

A middleware then verifies if the Authorization (the JWT token) is valid and saves the id that was inside it for later use: req.userId = token.id. This id will be used in the board creation to define its owner. So the id is coming from a (supposed) trusted source (a JWT token protected by a server-side secret).

Finally, with all the authentication done, the new board is created:

const data = {
    owner: req.userId,
    ...req.body
};

// simplified code to show the important stuff

const board = new Board(data);
await board.save();

// ...

(If you are not familiarized with the … (spread) operator, you can read about it here)

Like said before, the id extracted from the token is used as the owner field. This should guarantee that the owner is authenticated and prevent someone to create a board for another user. Right???

The problem

Take a look at the code again and try to find the problem. I only noticed it days later.

Look at the order of the data object creation. First, the owner is set, and then the user input. At first, I thought this was ok. If the user tried to send an invalid field, it would just be ignored by the model creation later. Using the spread operator I didn’t need to worry about checking if the input had invalid fields, and if a new field was later added, no new code would be required in this function.

However, what I didn’t think at first was: what if a malicious user sends the following input: {"name": "evil board", "owner": <the id of another user>}?

Because of the order that the data object was being created, the owner field would be changed to the one from the user input. Now someone could create boards (and tasks too, there was the same problem in the tasks endpoint) as someone else. It is not THAT bad (only the creation endpoint was vulnerable, it would be worse if the edition/deletion endpoints were affected too), but it sure is a vulnerability. It could be used to send abusive messages or spam to other users, for example.

The solution is very simple, obviously: just change the order. However, I was really amazed to see such a simple thing as a syntax sugar causing a vulnerability.

It made me remember of a vulnerability in Twitter where sending the same field twice in the request lead to a vulnerability (I don’t remember exactly what it was, but I think it could be exploited to trick other users to follow you thinking they were following someone else). It is not the same concept, but it sure looks similar.

I will surely be more alert to things like this in the future.