Validate the used query before running it

This commit is contained in:
rijkvanzanten
2020-10-14 15:31:47 -04:00
parent a18cd0ec1d
commit 1e628d8915
4 changed files with 125 additions and 9 deletions

View File

@@ -5,6 +5,7 @@
import { RequestHandler } from 'express';
import { sanitizeQuery } from '../utils/sanitize-query';
import { validateQuery } from '../utils/validate-query';
const sanitizeQueryMiddleware: RequestHandler = (req, res, next) => {
req.sanitizedQuery = {};
@@ -13,15 +14,16 @@ const sanitizeQueryMiddleware: RequestHandler = (req, res, next) => {
req.sanitizedQuery = sanitizeQuery(
{
fields: req.query.fields || '*',
...req.query
...req.query,
},
req.accountability || null
);
Object.freeze(req.sanitizedQuery);
validateQuery(req.sanitizedQuery);
return next();
};
export default sanitizeQueryMiddleware;

View File

@@ -2,7 +2,12 @@ import { Filter, Accountability } from '../types';
import { deepMap } from './deep-map';
export function parseFilter(filter: Filter, accountability: Accountability | null) {
return deepMap(filter, (val: any) => {
return deepMap(filter, (val: any, key: string) => {
if (val === 'true') return true;
if (val === 'false') return false;
if (key === '_in' || key === '_nin') return val.split(',').filter((val: any) => val);
if (val === '$NOW') return new Date();
if (val === '$CURRENT_USER') return accountability?.user || null;
if (val === '$CURRENT_ROLE') return accountability?.role || null;

View File

@@ -2,7 +2,10 @@ import { Accountability, Query, Sort, Filter, Meta } from '../types';
import logger from '../logger';
import { parseFilter } from '../utils/parse-filter';
export function sanitizeQuery(rawQuery: Record<string, any>, accountability: Accountability | null) {
export function sanitizeQuery(
rawQuery: Record<string, any>,
accountability: Accountability | null
) {
const query: Query = {};
if (rawQuery.limit !== undefined) {
@@ -49,11 +52,7 @@ export function sanitizeQuery(rawQuery: Record<string, any>, accountability: Acc
query.search = rawQuery.search;
}
if (
rawQuery.export &&
typeof rawQuery.export === 'string' &&
['json', 'csv'].includes(rawQuery.export)
) {
if (rawQuery.export) {
query.export = rawQuery.export as 'json' | 'csv';
}

View File

@@ -0,0 +1,110 @@
import { Query } from '../types';
import Joi from 'joi';
import { InvalidQueryException } from '../exceptions';
const querySchema = Joi.object({
fields: Joi.array().items(Joi.string()),
sort: Joi.array().items(
Joi.object({
column: Joi.string(),
order: Joi.string().valid('asc', 'desc'),
})
),
filter: Joi.object({}).unknown(),
limit: Joi.number(),
offset: Joi.number(),
page: Joi.number(),
single: Joi.boolean(),
meta: Joi.array().items(Joi.string().valid('total_count', 'result_count')),
search: Joi.string(),
export: Joi.string().valid('json', 'csv'),
deep: Joi.link('#query'),
}).id('query');
export function validateQuery(query: Query) {
const { error } = querySchema.validate(query);
if (query.filter && Object.keys(query.filter).length > 0) {
validateFilter(query.filter);
}
if (error) {
throw new InvalidQueryException(error.message);
}
return query;
}
function validateFilter(filter: Query['filter']) {
if (!filter) throw new InvalidQueryException('Invalid filter object');
for (const [key, nested] of Object.entries(filter)) {
if (key === '_and' || key === '_or') {
nested.forEach(validateFilter);
} else if (key.startsWith('_')) {
const value = nested;
switch (key) {
case '_eq':
case '_neq':
case '_contains':
case '_ncontains':
case '_gt':
case '_gte':
case '_lt':
case '_lte':
default:
validateFilterPrimitive(value, key);
break;
case '_in':
case '_nin':
validateList(value, key);
break;
case '_null':
case '_nnull':
case '_empty':
case '_nempty':
validateBoolean(value, key);
break;
}
} else {
validateFilter(nested);
}
}
}
function validateFilterPrimitive(value: any, key: string) {
if ((typeof value === 'string' || typeof value === 'number') === false) {
throw new InvalidQueryException(
`The filter value for "${key}" has to be a string or a number`
);
}
if (Number.isNaN(value)) {
throw new InvalidQueryException(`The filter value for "${key}" is not a valid number`);
}
if (typeof value === 'string' && value.length === 0) {
throw new InvalidQueryException(
`You can't filter for an empty string in "${key}". Use "_empty" or "_nempty" instead`
);
}
return true;
}
function validateList(value: any, key: string) {
if (Array.isArray(value) === false || value.length === 0) {
throw new InvalidQueryException(`"${key}" has to be an array of values`);
}
return true;
}
function validateBoolean(value: any, key: string) {
if (typeof value !== 'boolean') {
throw new InvalidQueryException(`"${key}" has to be a boolean`);
}
return true;
}