Preventing Privilege Escalation in NoSQL & SQL: Secure Node.js Practices

Privilege Escalation is a vulnerability in which a user of the system could “escalate” themselves to a higher-privileged role, such as admin, through some loophole in the backend code. Taking Node.js example, let’s see how it could be done, and what are the best practices to avoid it.

To explore more possible attacks and their solutions, take a look at my post web security checklist.

How Privilege Escalation Can Happen

Assume that in the req.body, the user has maliciously included admin as true.

{
  "username": "John",
  "admin": true,
}

In NoSQL

Take the example of user schema in Mongoose.

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
  admin: {
    type: Boolean,
    default: false,
  },
  // ...
});

const User = mongoose.model("User", userSchema);

The “admin” field when set to true means the user has the privilege of admin. (Alternatively, you could have a “role” field, that could take “user”, “admin”, and “super admin”. Either way, the vulnerability works.)

For any put or patch request, if the incoming parameters are not scrutinized and the proper checks are not in place, the malicious user can escalate/increase their privilege by themselves. Let’s take the example of the updateUser route, where only the provided fields get changed:

const updateUser = (req, res) => {
  doc = await User.findOneAndUpdate({id: req.params.id}, req.body);
  // Or with destructuring
  doc = await User.findOneAndUpdate({id: req.params.id}, {updatedAt: new Date(), updatedBy: req.user._id, ...req.body});
}

Since “admin” is a valid field in user, you cannot stop it from saving.

In SQL

Privilege Escalation can happen in SQL where an ORM is involved that takes JSON and converts it into a query under the hood. Take this or a similar variation as an example:

const updateUser = (req, res) => {
  await dataSource
    .createQueryBuilder()
    .update(User)
    .set(req.body)
    // Or with destructuring
    // .set({updatedAt: new Date(), updatedBy: req.user._id, ...req.body})
    .where("id = :id", { id: req.params.id }) request
    .execute();
}

Again, this will result in escalating the user privilege because the query is valid according to the SQL schema and we’re responsible for letting it go to the database as is.

Solutions to Avoid Privilege Escalation for SQL and NoSQL

1. Be Explicit and Don’t Use Destructuring Statements or Pass the Whole Object

The first layer of defense: just pick and choose the parameters you’re interested in, instead of assuming the incoming data is accurate and destructuring or passing it as is. Being explicit is a very good and secure practice.

Tip: Use different names for parameters and database fields. Although not always a viable or desirable solution, it could act as a layer of defense, since you can’t possibly mix this up. Even if not for all the fields, at least for the critical ones. For example, keep the database field as “role”, but expect “userRole” in the request. This way, you have to explicitly assign it before persisting in the database.

2. Ensure No Extra Parameter Is Present

Although being explicit is a good practice and line of defense, the best way to keep yourself safe from privilege escalation is to not accept parameters that are not supposed to be there. NestJS provides a built-in DTO mechanism that is used along with the application-level configuration to disallow extra parameters in the request (you get an error if you do pass anything extra). You can do similar checks in Express middleware, or in the case of Lambda functions, a yup or JSON schema validation. This is by far the best way to secure yourself from privilege escalation.

3. Implement Role Based Access Control (RBAC)

For any critical part of your system, this should be a given. A user should not be allowed to change roles if they don’t have the right privileges to begin with. This can be taken care of at the middleware level by checking the user’s current role, or in the final method itself. For example:

if (req.user.role !== "admin" && req.body.role) {
  return res.status(403).json({ error: "Unauthorized to modify role" });
}

// Or using middleware (in Express)
const protectRoleUpdate = (req, res, next) => {
  if (req.body.role && req.user.role !== "admin") {
    return res.status(403).json({ error: "Unauthorized to modify role" });
  }
  next();
};

app.put("/users/:id", protectRoleUpdate, async (req, res) => {
  // Update logic here
});

Now even if you destructure the code or pass the req.body as is (which you still shouldn’t do as a good practice), you are secure from privilege escalation.




See also

When you purchase through links on techighness.com, I may earn an affiliate commission.
We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies. More info cookie script