mirror of
https://github.com/directus/directus.git
synced 2026-01-23 15:07:55 -05:00
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:
6
.changeset/rare-deers-clap.md
Normal file
6
.changeset/rare-deers-clap.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@directus/app": patch
|
||||
"@directus/api": patch
|
||||
---
|
||||
|
||||
Prevented parsing non-decimal values in search query
|
||||
122
api/src/utils/apply-query.test.ts
Normal file
122
api/src/utils/apply-query.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user