Prevent parsing non-decimal values in search (#18383)

* prevent parsing non-decimal values in search

* Create rare-deers-clap.md

* Adjust format of changeset

* Add explaining comment to return logic of validateNumber

* remove float with zeros check

---------

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
This commit is contained in:
Brainslug
2023-05-03 15:42:28 +02:00
committed by GitHub
parent c74b5da7b9
commit a9f397de1f
3 changed files with 140 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
---
"@directus/app": patch
"@directus/api": patch
---
Prevented parsing non-decimal values in search query

View File

@@ -0,0 +1,122 @@
import { expect, vi, test, describe } from 'vitest';
import { applySearch } from './apply-query.js';
import type { SchemaOverview } from '@directus/types';
function mockDatabase() {
const self: Record<string, any> = {
andWhere: vi.fn(() => self),
orWhere: vi.fn(() => self),
orWhereRaw: vi.fn(() => self),
};
return self;
}
describe('applySearch', () => {
const FAKE_SCHEMA: SchemaOverview = {
collections: {
test: {
collection: 'test',
primary: 'id',
singleton: false,
sortField: null,
note: null,
accountability: null,
fields: {
text: {
field: 'text',
defaultValue: null,
nullable: false,
generated: false,
type: 'text',
dbType: null,
precision: null,
scale: null,
special: [],
note: null,
validation: null,
alias: false,
},
float: {
field: 'float',
defaultValue: null,
nullable: false,
generated: false,
type: 'float',
dbType: null,
precision: null,
scale: null,
special: [],
note: null,
validation: null,
alias: false,
},
integer: {
field: 'integer',
defaultValue: null,
nullable: false,
generated: false,
type: 'integer',
dbType: null,
precision: null,
scale: null,
special: [],
note: null,
validation: null,
alias: false,
},
id: {
field: 'id',
defaultValue: null,
nullable: false,
generated: false,
type: 'uuid',
dbType: null,
precision: null,
scale: null,
special: [],
note: null,
validation: null,
alias: false,
},
},
},
},
relations: [],
};
test.each(['0x56071c902718e681e274DB0AaC9B4Ed2d027924d', '0b11111', '0.42e3', 'Infinity', '42.000'])(
'Prevent %s from being cast to number',
async (number) => {
const db = mockDatabase();
db['andWhere'].mockImplementation((callback: () => void) => {
// detonate the andWhere function
callback.call(db);
return db;
});
await applySearch(FAKE_SCHEMA, db as any, number, 'test');
expect(db['andWhere']).toBeCalledTimes(1);
expect(db['orWhere']).toBeCalledTimes(0);
expect(db['orWhereRaw']).toBeCalledTimes(1);
}
);
test.each(['1234', '-128', '12.34'])('Casting number %s', async (number) => {
const db = mockDatabase();
db['andWhere'].mockImplementation((callback: () => void) => {
// detonate the andWhere function
callback.call(db);
return db;
});
await applySearch(FAKE_SCHEMA, db as any, number, 'test');
expect(db['andWhere']).toBeCalledTimes(1);
expect(db['orWhere']).toBeCalledTimes(2);
expect(db['orWhereRaw']).toBeCalledTimes(1);
});
});

View File

@@ -766,7 +766,11 @@ export async function applySearch(
this.orWhereRaw(`LOWER(??) LIKE ?`, [`${collection}.${name}`, `%${searchQuery.toLowerCase()}%`]);
} else if (['bigInteger', 'integer', 'decimal', 'float'].includes(field.type)) {
const number = Number(searchQuery);
if (!isNaN(number)) this.orWhere({ [`${collection}.${name}`]: number });
// only cast finite base10 numeric values
if (validateNumber(searchQuery, number)) {
this.orWhere({ [`${collection}.${name}`]: number });
}
} else if (field.type === 'uuid' && validate(searchQuery)) {
this.orWhere({ [`${collection}.${name}`]: searchQuery });
}
@@ -774,6 +778,13 @@ export async function applySearch(
});
}
function validateNumber(value: string, parsed: number) {
if (isNaN(parsed) || !Number.isFinite(parsed)) return false;
// casting parsed value back to string should be equal the original value
// (prevent unintended number parsing, e.g. String(7) !== "ob111")
return String(parsed) === value;
}
export function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void {
for (const [operation, fields] of Object.entries(aggregate)) {
if (!fields) continue;