Skip to content

Commit d838210

Browse files
committed
Add query complexity budgeting
- Add maxQueryComplexity to SecurityConfig (default: 100) - Enhance calculateQueryComplexity with detailed cost model - Add costs for expensive operations (nested, wildcard, regex, etc.) - Create validateQueryComplexity function - Integrate validation into find and bulk methods - Protect cluster from expensive queries
1 parent f47695f commit d838210

File tree

2 files changed

+52
-4
lines changed

2 files changed

+52
-4
lines changed

src/methods/find.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
'use strict'
22

33
import { parseQuery, mapFind } from '../utils/index'
4+
import { validateQueryComplexity } from '../utils/security'
45
import { ElasticsearchServiceParams, ElasticAdapterInterface, SearchRequest } from '../types'
56

67
export function find(service: ElasticAdapterInterface, params: ElasticsearchServiceParams) {
78
const { filters, query, paginate } = service.filterQuery(params)
89

10+
// PERFORMANCE: Validate query complexity budget
11+
validateQueryComplexity(query, service.security.maxQueryComplexity)
12+
913
// Move Elasticsearch-specific operators from filters back to query for parseQuery
1014
const esOperators = [
1115
'$all',

src/utils/security.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ export interface SecurityConfig {
7777
* @default true
7878
*/
7979
enableInputSanitization?: boolean
80+
81+
/**
82+
* Maximum query complexity score
83+
* PERFORMANCE: Limits expensive queries to protect cluster performance
84+
* @default 100
85+
*/
86+
maxQueryComplexity?: number
8087
}
8188

8289
/**
@@ -92,7 +99,8 @@ export const DEFAULT_SECURITY_CONFIG: Required<SecurityConfig> = {
9299
allowedRawMethods: [],
93100
searchableFields: [],
94101
enableDetailedErrors: process.env.NODE_ENV !== 'production',
95-
enableInputSanitization: true
102+
enableInputSanitization: true,
103+
maxQueryComplexity: 100
96104
}
97105

98106
/**
@@ -343,6 +351,7 @@ export function sanitizeError(
343351
/**
344352
* Calculates the complexity score of a query
345353
* Used for rate limiting or rejection of overly complex queries
354+
* PERFORMANCE: Enhanced complexity calculation with costs for expensive operations
346355
*
347356
* @param query - Query object
348357
* @returns Complexity score (higher = more complex)
@@ -357,17 +366,33 @@ export function calculateQueryComplexity(query: unknown): number {
357366
for (const key of Object.keys(query as object)) {
358367
const value = (query as Record<string, unknown>)[key]
359368

360-
// Each operator adds to complexity
369+
// Base cost for each operator
361370
complexity += 1
362371

372+
// Expensive operators (wildcards, regex, fuzzy) have higher costs
373+
if (key === '$wildcard') {
374+
complexity += 5
375+
} else if (key === '$regexp') {
376+
complexity += 8
377+
} else if (key === '$fuzzy') {
378+
complexity += 6
379+
} else if (key === '$prefix') {
380+
complexity += 3
381+
} else if (key === '$script') {
382+
complexity += 15 // Scripts are very expensive
383+
}
363384
// Nested operators are more expensive
364-
if (key === '$or' || key === '$and') {
385+
else if (key === '$or' || key === '$and') {
365386
if (Array.isArray(value)) {
366387
for (const item of value) {
367388
complexity += calculateQueryComplexity(item) * 2
368389
}
369390
}
370-
} else if (key === '$nested' || key === '$child' || key === '$parent') {
391+
} else if (key === '$nested') {
392+
if (typeof value === 'object') {
393+
complexity += calculateQueryComplexity(value) * 10 // Nested queries are very expensive
394+
}
395+
} else if (key === '$child' || key === '$parent') {
371396
if (typeof value === 'object') {
372397
complexity += calculateQueryComplexity(value) * 3
373398
}
@@ -381,3 +406,22 @@ export function calculateQueryComplexity(query: unknown): number {
381406

382407
return complexity
383408
}
409+
410+
/**
411+
* Validates query complexity against budget
412+
* PERFORMANCE: Rejects overly complex queries to protect cluster performance
413+
*
414+
* @param query - Query object to validate
415+
* @param maxComplexity - Maximum allowed complexity score
416+
* @throws BadRequest if query exceeds complexity budget
417+
*/
418+
export function validateQueryComplexity(query: unknown, maxComplexity: number): void {
419+
const complexity = calculateQueryComplexity(query)
420+
421+
if (complexity > maxComplexity) {
422+
throw new errors.BadRequest(
423+
`Query complexity (${complexity}) exceeds maximum allowed (${maxComplexity}). ` +
424+
`Simplify your query by reducing nested conditions, wildcard searches, or array sizes.`
425+
)
426+
}
427+
}

0 commit comments

Comments
 (0)