How to make “Input validation” easy for your devs

Mohamed AboElKheir
AppSec Untangled
Published in
5 min readJul 22, 2024

What is Input validation

Input validation is one of the basic security controls that help protect against a wide range of web application attacks (e.g. SQL injection, Command injection, Directory traversal, .. etc). The goal of input validation is to discard any input that doesn’t look valid before it reaches a dangerous sink (e.g. an SQL statement).

What qualifies as input depends on the type of the application, e.g. for a typical REST API accepting HTTP requests, input parameters could come from:

  • Request headers
  • Cookies
  • GET parameters
  • Route parameters, e.g. for the route /api/resource/:resourceId the resource id is in the path/route e.g. for /api/resource/12345678 the resourceId is 12345678
  • Request body, e.g. fields in a JSON object

For each input parameter validation can take many forms but the most common ones are:

  • List of allowed values. e.g. for a type input parameter could only be one of the valid types (e.g. email, chat, or SMS).
  • A pattern. e.g. for a resource_id input parameter should be in the form of a UUID.
  • Maximum length with a list of allowed characters. e.g. for a description input parameter

Why Input validation is needed

This is effective because most attacks need some kind of weird-looking payload that includes special characters being passed as input (the source) and reaching a dangerous function (the sink). For example, an SQL injection payload may look like value'; DROP TABLE users; --. However, when input validation is applied these kinds of payloads are usually discarded before reaching the dangerous sink.

Input validation

NOTE: While input validation is a useful security control, it doesn’t replace the need to replace dangerous sinks with safe ones. For e.g. for SQL injection you should always use parameterized queries to avoid SQL injection. Input validation should be used as a security-in-depth control just in case a dangerous sink is missed.

Paved road for Input validation

Despite the value and the relatively low complexity of input validation, many developers skip implementing it for their applications. This is because we usually follow the least friction path to our goal, and input validation is not generally necessary for the application to work as expected, hence developers ignore it or de-prioritize it to focus on the actual logic of the application.

This is why it is important for us application security engineers not to only focus on the importance of input validation, but also on how to make it easy for developers to implement it. Finding a way that makes it straightforward, easy to implement, and easy to audit a security control significantly reduces friction and increases adoption. In other words, we should find a “Paved road” for our developers to implement input validation.

For input validation, one way to do that is to bundle the input validation logic, and the behavior when input validation fails (e.g. return 400 status code with a custom error message) into some sort of middleware, and then use this middleware for our routes. This way developers only need to add this middleware to the routes and specify the expected parameters for each route with the corresponding type of validation.

Express as an example

As an example, let’s assume we are using Node.JS and Express framework for our backend. We can implement this middleware which uses the Joi validation library under the hood. The middleware can look something like:

const Joi = require('joi');
const validate = (schema) => {
return (req, res, next) => {
const sources = ['body', 'query', 'params', 'headers','cookies'];
const validationResults = sources.map(source => {
if (schema[source]) {
return schema[source].validate(req[source], { abortEarly: false });
}
return { error: null };
});
const errors = validationResults.reduce((acc, result) => {
if (result.error) {
acc.push(...result.error.details);
}
return acc;
}, []);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
next();
};
};
module.exports = validate;

Now, all what developers need to do is to specify a schema that defines the inputs coming from the request body, query parameters, path parameters, headers and cookies, e.g.

const createUserSchema = {
body: Joi.object({
username: Joi.string().email().required(), // only emails are accepted
password: Joi.string().min(6).required(), // minimum length is 6 characters
}),
headers: Joi.object({
'test1': Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(), // a regular expression
'test2': Joi.string().valid('value1','value2','value3').optional(), // list of allowed values
}).unknown(true), // Allow other headers
cookies: Joi.object({
'cookie1': Joi.uuid().required(), // only uuids are accepted
'cookie2': Joi.string().valid('value1','value2','value3').optional(),// list of allowed values
}),
params: Joi.object(),
query: Joi.object()
};

This scheme specifies the body parameters, headers, and cookies expected and their allowed values/patterns. Any other parameters or any invalid value will automatically return a 400 response code.

Then the developers just need to use the middleware with the schema when defining the route, e.g.:

app.post('/user', validate(createUserSchema), (req, res) => {
// Generate a unique ID for the user (for demonstration purposes)
const userId = uuidv4();

// Create a new user object from the request body
const newUser = {
userId: userId,
username: req.body.username,
password: req.body.password
};
// Add the new user to the users object using the generated ID
users.push(newUser);
// Respond with the newly created user object
res.json(newUser);
});

Auditing the use of the middleware

Also, we can use a tool like Semgrep to audit the use of the middleware through all routes using a custom rule, to show a simple example we can use a rule like the below (Note this is just example which covers one way of defining routes in Express, for production the rule needs to be extended to include all other ways of defining routes).

rules:
- id: express-route-without-validate-middleware
patterns:
- pattern: |
router.route($ROUTE).$METHOD(...)
- pattern-not-inside: |
router.route($ROUTE).$METHOD(validate(...), $HANDLER)
message: "Route declared without validate middleware"
severity: WARNING
languages: [javascript, typescript]
metadata:
category: security
technology: express
description: "Add validate middleware to this route"

When forgetting to add the middleware, this rule would generate a finding for the developers.

Conclusion

The above example is just one way to create a paved road to make input validation easier for your developers, obviously based on what languages and frameworks your developers are using you may need to adapt your approach. My recommendation is to have a discussion with your dev teams and security champions to decide on the best way to implement a solution that works for your organization, and make your goal for the agreed solution to be easy to use, and easy to audit as shown in the above example.

--

--