DEV Community

Cover image for Input Sanitation
Shahar Kedar
Shahar Kedar

Posted on • Edited on

Input Sanitation

Zod has a really nice feature that allows us to define, for schemas that describe objects, how properties not defined in the schema should be treated. We can choose one of 3 modes:

  • Strip: Zod will strip out unrecognized keys during parsing. This is the default behaviour.
  • Passthrough: Zod will keep unrecognized keys and will not validate them.
  • Strict: Zod will return an error for any unrecognized key.

All three modes have their uses, but in this post I will focus on strip and strict, who can help with what is called "Input Sanitation".

What is input sanitation?

Input sanitation is a critical security practice aimed at preventing malicious users from injecting harmful data into our software. This practice involves validating and cleaning up the data received from the user before processing or storing it.

Example scenario: unauthorized account modification

Imagine a web application that allows users to update their profile information, including their email but not their user role, which is intended to be controlled only by administrators. The application receives an object with the fields to be updated and directly passes it to the database query without properly sanitizing the input to remove or restrict fields.

Vulnerable code snippet:

app.post('/updateProfile', function(req, res) {
    // Assuming req.body is something like {email: "newemail@example.com"}
    const updates = req.body;
    const userId = req.session.userId; // The ID of the currently logged-in user

    // Update the user profile with the provided fields
    db.collection('users').updateOne({ _id: userId }, { $set: updates }, function(err, result) {
        if (err) {
            // handle error
        } else {
            // success
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

An attacker discovers this endpoint and decides to send a modified request that includes an additional property, role, in an attempt to escalate their privileges:

{
    "email": "attacker@example.com",
    "role": "admin"
}
Enter fullscreen mode Exit fullscreen mode

By sending this payload, the attacker could potentially change their user role to "admin", assuming the application does not properly check the fields that are being updated. This happens because the database command directly uses the object from the request, allowing any properties provided to be included in the $set operation.

How can Zod help?

We can use one of the modes mentioned above to prevent this attack. Let's see what would be the behavior of each mode:

Strip

Strip is the default mode of every schema and does not require any explicit configuration. We can change the vulnerable endpoint above in the following way:

const Updates = z.object({
  email: z.string()
})
app.post('/updateProfile', function(req, res) {
    const updates = Updates.parse(req.body);
    // log updates to see the result
    console.log("You shall not pass!", updates);
    const userId = req.session.userId; 

    // Update the user profile with the provided fields
    db.collection('users').updateOne({ _id: userId }, { $set: updates }, function(err, result) {
        if (err) {
            // handle error
        } else {
            // success
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

Now when an attacker sends the following body:

{
    "email": "attacker@example.com",
    "role": "admin"
}
Enter fullscreen mode Exit fullscreen mode

Zod will strip the role property from the body before passing it on to the update statement. We should expect to see the following log:

You shall not pass! { "email": "attacker@example.com" }
Enter fullscreen mode Exit fullscreen mode

Strict

We can also configure a schema to be strict, causing unrecognized keys to throw an error:

const Updates = z.object({
  email: z.string()
}).strict()
// ... rest of code
Enter fullscreen mode Exit fullscreen mode

Now calling the endpoint with the malicious payload will result in the following error:

ZodError: [
  {
    "code": "unrecognized_keys",
    "keys": [
      "role"
    ],
    "path": [],
    "message": "Unrecognized key(s) in object: 'role'"
  }
]
Enter fullscreen mode Exit fullscreen mode

In the context of input sanitation for security, using strict could be useful if we are looking to identify security breaches attempts as they happen.

Another interesting option is to use a mix of strict and strip:

const Updates = z.object({
  email: z.string()
})
const StrictUpdates = Updates.strict();
app.post('/updateProfile', function(req, res) {
  const parseResult = StrictUpdates.safeParse(req.body);
  if (!parseResult.success && parseResult.error.issues.some(issue => issue.code === ZodIssueCode.unrecognized_keys)) {
    console.error("Unrecognized keys in updates");
  }
  const updates = Updates.parse(req.body);    
  console.log("You shall not pass!", updates);
  // ... rest of code
});
Enter fullscreen mode Exit fullscreen mode

Notice that usage of safeParse instead of parse when using the StrictUpdates schema. safeParse allows us to validate input without throwing an error in case the input is invalid. In this case we use safeParse to identify and log unrecognized keys, but not fail the request.

Summary

Input sanitation is a very common and important security measure. Zod can help sanitize inputs in different ways - by silently dropping unrecognized keys or by throwing errors.

Next chapter we will learn how to define union types with Zod.

Top comments (0)