Introduction

In our previous courses, we took a deep dive into client-side validation when working on the user registration feature. However, it’s crucial to note that client-side validation can be bypassed. Attackers can deliberately circumvent client-side restrictions, emphasizing the importance of robust server-side validation. Let’s now shift our focus to the snippets part of our application and demonstrate secure server-side validation using TypeScript.

Understanding Server-Side Validation

Server-side validation is your final gatekeeper to ensure that data is clean and meets the expected requirements. Even if malicious users bypass client-side checks, robust server-side validation will stop unsafe or malformed data from compromising your application. TypeScript’s strict typing, combined with libraries like Zod, offers a structured way to define and enforce these rules. While TypeScript enforces type safety at compile time, ensuring that variables and functions receive the expected types before the code is run, it is erased at runtime. This means incoming user input is still just a raw JavaScript object, allowing attackers to send unexpected values, such as null, objects instead of strings, or excessively large inputs. Therefore, using runtime validation libraries like Zod is essential to dynamically enforce data constraints.

Vulnerable Code Example

Below is a snippet of our “save snippet” endpoint. Currently, there is no server-side validation in place to verify the data contained in the request body:

Since the snippet is directly created from user input, attackers could insert malicious data (e.g., harmful scripts) into these fields.

Exploiting the Vulnerability

Even with the correct token, the lack of server-side validation allows attackers to exploit the endpoint. They can bypass client-side validation and send raw requests with malicious payloads:

This request, despite being authenticated, can inject harmful scripts into the application. Without proper server-side validation, these payloads could be stored or rendered elsewhere, leading to cross-site scripting (XSS) or other injection-based exploits. This highlights the critical need for robust server-side validation to protect against such vulnerabilities.

Defining a Validation Schema with Zod

Zod is a TypeScript-first schema declaration and validation library. It allows developers to define schemas for data structures and validate data against these schemas. Zod is particularly useful for ensuring that incoming data meets specific criteria, which is crucial for maintaining application security and integrity.

Zod provides a simple and expressive way to define the shape and constraints of data. It supports a wide range of data types and validation rules, making it a powerful tool for both server-side and client-side validation. By using Zod, you can ensure that data adheres to the expected format before it is processed or stored, reducing the risk of security vulnerabilities and data corruption.

In the code snippet below, we define a Zod schema for validating snippet data:

  • z.object({...}): This function creates a schema for an object, specifying the expected structure and types of its properties.

  • title: z.string().min(3).max(100): This line defines the title property as a string with a minimum length of 3 characters and a maximum length of 100 characters. This ensures that the title is neither too short nor excessively long, which helps prevent certain types of attacks and data issues.

  • content: z.string().max(5000): The content property is defined as a string with a maximum length of 5000 characters. This constraint prevents overly large inputs that could lead to performance issues or buffer overflows.

  • language: z.enum([...]): The language property is defined as an enumeration, allowing only specific values: "typescript", "javascript", "python", "java", and "cpp". This restricts the input to a predefined set of valid options, preventing unexpected or harmful values from being processed.

By setting these boundaries and constraints, the Zod schema helps block dangerous or unexpected input, ensuring that only valid data is processed by the application. This approach enhances security by reducing the risk of injection attacks and other vulnerabilities.

Implementing Validation Middleware

Middleware functions in Express.js are functions that have access to the request (req), response (res), and the next middleware function in the application’s request-response cycle. They can execute code, modify the request and response objects, end the request-response cycle, and call the next middleware function. Middleware is a powerful way to handle tasks such as logging, authentication, and validation in a modular and reusable manner.

In the code snippet below, we create a middleware function to validate incoming data against the Zod schema:

  • const validateSnippet = (req, res, next) => { ... }: This defines a middleware function named validateSnippet. It takes three parameters: req (the request object), res (the response object), and next (a function to pass control to the next middleware).

  • snippetSchema.safeParse(req.body): This line uses the safeParse method from Zod to validate the request body against the snippetSchema. The safeParse method returns an object with a success property indicating whether the validation passed, and an error property containing details if it failed.

  • if (!parsed.success) { ... }: This conditional checks if the validation was unsuccessful. If parsed.success is false, it means the input data did not meet the schema's requirements.

  • return res.status(400).json({ error: "Invalid input", details: parsed.error.errors });: If validation fails, the middleware sends a 400 Bad Request response with an error message and details about the validation errors. This prevents the request from proceeding further, stopping unsafe or malformed data from reaching the database.

  • next();: If the validation is successful, the next() function is called to pass control to the next middleware function or route handler in the stack. This allows the request to continue through the application’s request-response cycle.

To enhance the middleware function, we can add logging to capture validation errors, which aids in debugging and monitoring. This addition will provide insights into why certain requests fail validation, allowing developers to address issues more effectively.

Explanation of the Logging Addition

  • console.error("Validation Error:", parsed.error.errors);: This line logs the validation errors to the console whenever validation fails. It provides detailed information about the nature of the validation error, including the specific field that failed and the reason for the failure.

Example Output

If a snippet is submitted with a title that is less than 3 characters, the console will output:

By implementing this middleware, you ensure that all incoming data is validated before it is processed by your application. This approach helps maintain data integrity and security by preventing invalid or malicious data from being stored or executed.

Securing the Endpoint with Validation

After defining the validation middleware, the next step is to incorporate it into the "save snippet" endpoint. This ensures that all incoming data is validated before any further processing occurs, thereby maintaining the integrity and security of your application.

Explanation

  • router.post('/', validateSnippet, async (req, res) => { ... }): The validateSnippet middleware is added as the second argument in the route definition. This ensures that the request body is validated against the snippetSchema before the main logic of the endpoint is executed.

  • Authorization Check: The endpoint first checks for the presence of an authorization header. If it's missing, a 401 Unauthorized response is returned. This step ensures that only authenticated users can proceed.

  • Token Verification: The token from the authorization header is verified using jwt.verify. If the token is invalid, a 401 Unauthorized response is returned. This step confirms the user's identity and permissions.

  • Data Processing: If the request passes both validation and authentication, the data is extracted from the request body and used to create a new snippet. The Snippet.create method is called with the validated data, ensuring that only safe and expected data is stored in the database.

By integrating the validateSnippet middleware into the endpoint, you create a robust layer of defense against invalid or malicious data. This approach not only protects your application from potential security threats but also ensures that the data stored in your system is consistent and reliable.

Client-Side vs. Server-Side Validation

• Client-Side Validation: Runs in the user’s browser before data is sent to the server, providing quick feedback to users and reducing unnecessary network traffic if the input is obviously invalid. However, because it runs on the client, it can be disabled or tampered with by attackers using tools like cURL or custom scripts.

• Server-Side Validation: Occurs on the server once data has been received. This acts as the ultimate authority for data integrity, preventing malicious or malformed data from entering the backend systems or databases. Even if client-side checks are bypassed, server-side validation remains in place to protect your application.

Both forms of validation can coexist to deliver a better user experience and enforce strict security measures. While you should strive to provide immediate feedback through client-side checks, you must never skip or rely solely on them. Proper server-side validation is essential for protecting your application against sophisticated attacks.

Conclusion and Next Steps

By applying Zod-based validation in conjunction with TypeScript, you reinforce your application’s defenses at the server level. This complements any previous client-side checks you implemented (such as those used in registration flows) and helps maintain robust security for all new code dealing with user-provided data.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal