Skip to content

Use Strict Schema

Guard Your Input Sources with Strict Schemas

The following is an example of how the zod library can be used to validate incoming query parameters as part of a user input in an HTTP request. However there are some caveats but first let’s unfold the primary use-case:

import { z } from "zod";
app.get("/users", (req, res) => {
const querySchema = z.object({
filter: z.string().optional(),
});
const validatedQuery = querySchema.parse(req.query);
});

In the above example, while it is possible and encouraged to use TypeScript during development-time to ensure that the query string filter is of type string, it is not any less important, if not more, to validate the incoming query parameters at runtime to ensure that the data is in the expected format. The way to do that is through libraries like Zod.

However, the above example is not sufficient enough in its schema strictness. For example, consider that the filter query parameter is set to __proto__. Depending on your code-base and how it handles the filter variable, it may be benign or it may be a severe prototype pollution security vulnerability that allows adversaries to escalate the attack to a full-blown remote code execution.

Strict Schema Values with Zod

A more complete and stricter schema would be to specifically define the expected values of the query parameter, as much as possible. For example, if the filter query parameter is expected to be a string that is either active or inactive, then the schema should be updated to reflect that:

import { z } from "zod";
app.get("/users", (req, res) => {
const querySchema = z.object({
filter: z.enum(["active", "inactive"]).optional(),
});
const validatedQuery = querySchema.parse(req.query);
});

If you’re unable to specify the exact values it is recommended to take other security controls to ensure that the input is sanitized and validated before being used in your application.

Strict Schema Field with Zod

Another way in which Zod and other schema related libraries that help with validation can be weak in their schema strictness is that they do not enforce the schema matches exactly the expected fields. Schema strictness isn’t just about matching existing fields, but rather to make sure that new fields that are not part of the schema, are not allowed to be passed through.

This sort of schema strictness may seem insignificant because new and undeclared fields are potentially not handled, however this is exactly the common attack vector for Mass Assignments vulnerabilities. For example, consider the following schema:

async saveUser (req: Request, res: Response) => {
const userObject = req.body;
userObject.id = Number.parseInt(req.params.id, 10);
const validationResult = UserSchema.safeParse(userObject);
if (!validationResult.success) {}
const user: User = userObject;
const serviceResponse = await userService.saveUser(user);
return handleServiceResponse(serviceResponse, res);
}

Expected user input may be sent as follows:

Terminal window
$ curl -X POST -H 'Content-Type: application/json' http://localhost:8080/users/1/profile -d '{
"name": "Liran",
"email": "liran@example.com",
"age": 30
}'

However, what if the service being called by the userService.saveUser(user) function call is taking the user input object as-is. If that’s the case, then an adversary can send the following payload:

Terminal window
$ curl -X POST -H 'Content-Type: application/json' http://localhost:8080/users/1/profile -d '{
"name": "Liran",
"email": "liran@example.com",
"age": 30,
"isAdmin": true
}'

And just like that, if the isAdmin field exists on the underlying database schema, the adversary would be successful in their attack that creates or saves a user’s profile information with extra persistent fields that were not intended for, and were not strictly validated against as part of the schema.