diff --git a/.changeset/rare-deers-clap.md b/.changeset/rare-deers-clap.md new file mode 100644 index 0000000000..e5a1e02d95 --- /dev/null +++ b/.changeset/rare-deers-clap.md @@ -0,0 +1,6 @@ +--- +"@directus/app": patch +"@directus/api": patch +--- + +Prevented parsing non-decimal values in search query diff --git a/api/src/utils/apply-query.test.ts b/api/src/utils/apply-query.test.ts new file mode 100644 index 0000000000..ec0ad42fc5 --- /dev/null +++ b/api/src/utils/apply-query.test.ts @@ -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 = { + 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); + }); +}); diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index 7a3c6d6a88..92af12683d 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -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;