Input Sanitation
Tags: programming, typescript, node, zod
Published: 2024-03-22 07:06:52

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:
```typescript
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
        }
    });
});
```
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:

```json
{
    "email": "attacker@example.com",
    "role": "admin"
}
```
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:

```typescript
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
        }
    });
});
```
Now when an attacker sends the following body:
```json
{
    "email": "attacker@example.com",
    "role": "admin"
}
```
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" }
```

### Strict

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

```typescript
const Updates = z.object({
  email: z.string()
}).strict()
// ... rest of code
```
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'"
  }
]
```
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`:

```typescript
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
});
```

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](- [Chapter 4: Union Types](https://dev.to/shaharke/zod-zero-to-hero-chapter-4-513c)
) we will learn how to define union types with Zod.