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
}
});
});
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"
}
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
}
});
});
Now when an attacker sends the following body:
{
"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:
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
:
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 we will learn how to define union types with Zod.
Top comments (0)