@gblikas/querykit 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +5 -7
- package/README.md +76 -0
- package/dist/parser/parser.d.ts +34 -0
- package/dist/parser/parser.js +164 -6
- package/dist/security/types.d.ts +48 -0
- package/dist/security/types.js +2 -0
- package/dist/security/validator.d.ts +35 -0
- package/dist/security/validator.js +108 -0
- package/examples/qk-next/app/globals.css +23 -0
- package/examples/qk-next/app/hooks/use-viewport-info.ts +89 -0
- package/examples/qk-next/app/layout.tsx +26 -7
- package/examples/qk-next/app/page.tsx +423 -121
- package/examples/qk-next/lib/utils.ts +74 -0
- package/examples/qk-next/package.json +5 -3
- package/examples/qk-next/pnpm-lock.yaml +112 -47
- package/package.json +5 -1
- package/src/parser/parser.test.ts +209 -1
- package/src/parser/parser.ts +234 -25
- package/src/security/types.ts +52 -0
- package/src/security/validator.test.ts +368 -0
- package/src/security/validator.ts +117 -0
|
@@ -331,6 +331,374 @@ describe('QuerySecurityValidator', () => {
|
|
|
331
331
|
});
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
describe('validateDenyValues', () => {
|
|
335
|
+
it('should accept queries when no denyValues are configured', () => {
|
|
336
|
+
const validator = new QuerySecurityValidator({});
|
|
337
|
+
const query = parser.parse('status:"deleted"');
|
|
338
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should accept queries when value is not in denyValues list', () => {
|
|
342
|
+
const validator = new QuerySecurityValidator({
|
|
343
|
+
denyValues: {
|
|
344
|
+
status: ['deleted', 'banned']
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const query = parser.parse('status:"active"');
|
|
348
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should reject queries with denied string values', () => {
|
|
352
|
+
const validator = new QuerySecurityValidator({
|
|
353
|
+
denyValues: {
|
|
354
|
+
status: ['deleted', 'banned']
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
const query = parser.parse('status:"deleted"');
|
|
358
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
359
|
+
expect(() => validator.validate(query)).toThrow(
|
|
360
|
+
'Invalid query parameters'
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should reject queries with denied numeric values', () => {
|
|
365
|
+
const validator = new QuerySecurityValidator({
|
|
366
|
+
denyValues: {
|
|
367
|
+
priority: [0, -1]
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
const mockQuery: QueryExpression = {
|
|
371
|
+
type: 'comparison',
|
|
372
|
+
field: 'priority',
|
|
373
|
+
operator: '==',
|
|
374
|
+
value: 0
|
|
375
|
+
};
|
|
376
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
377
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
378
|
+
'Invalid query parameters'
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should reject queries with denied boolean values', () => {
|
|
383
|
+
const validator = new QuerySecurityValidator({
|
|
384
|
+
denyValues: {
|
|
385
|
+
isAdmin: [true]
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
const mockQuery: QueryExpression = {
|
|
389
|
+
type: 'comparison',
|
|
390
|
+
field: 'isAdmin',
|
|
391
|
+
operator: '==',
|
|
392
|
+
value: true
|
|
393
|
+
};
|
|
394
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
395
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
396
|
+
'Invalid query parameters'
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should reject queries with denied null values', () => {
|
|
401
|
+
const validator = new QuerySecurityValidator({
|
|
402
|
+
denyValues: {
|
|
403
|
+
deletedAt: [null]
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const mockQuery: QueryExpression = {
|
|
407
|
+
type: 'comparison',
|
|
408
|
+
field: 'deletedAt',
|
|
409
|
+
operator: '==',
|
|
410
|
+
value: null
|
|
411
|
+
};
|
|
412
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
413
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
414
|
+
'Invalid query parameters'
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should reject queries with denied values in IN operator arrays', () => {
|
|
419
|
+
const validator = new QuerySecurityValidator({
|
|
420
|
+
denyValues: {
|
|
421
|
+
role: ['superadmin', 'system']
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
const mockQuery: QueryExpression = {
|
|
425
|
+
type: 'comparison',
|
|
426
|
+
field: 'role',
|
|
427
|
+
operator: 'IN',
|
|
428
|
+
value: ['admin', 'superadmin'] // Contains 'superadmin' which is denied
|
|
429
|
+
};
|
|
430
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
431
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
432
|
+
'Invalid query parameters'
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should accept IN operator arrays without denied values', () => {
|
|
437
|
+
const validator = new QuerySecurityValidator({
|
|
438
|
+
denyValues: {
|
|
439
|
+
role: ['superadmin', 'system']
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
const mockQuery: QueryExpression = {
|
|
443
|
+
type: 'comparison',
|
|
444
|
+
field: 'role',
|
|
445
|
+
operator: 'IN',
|
|
446
|
+
value: ['admin', 'user', 'moderator']
|
|
447
|
+
};
|
|
448
|
+
expect(() => validator.validate(mockQuery)).not.toThrow();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should validate denied values in nested logical expressions', () => {
|
|
452
|
+
const validator = new QuerySecurityValidator({
|
|
453
|
+
denyValues: {
|
|
454
|
+
status: ['deleted']
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
const query = parser.parse('name:"John" AND status:"deleted"');
|
|
458
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
459
|
+
expect(() => validator.validate(query)).toThrow(
|
|
460
|
+
'Invalid query parameters'
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should validate denied values in complex nested expressions', () => {
|
|
465
|
+
const validator = new QuerySecurityValidator({
|
|
466
|
+
denyValues: {
|
|
467
|
+
role: ['superadmin']
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
const query = parser.parse(
|
|
471
|
+
'(name:"John" OR name:"Jane") AND (role:"superadmin" OR status:"active")'
|
|
472
|
+
);
|
|
473
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
474
|
+
expect(() => validator.validate(query)).toThrow(
|
|
475
|
+
'Invalid query parameters'
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should support dot-notation field names in denyValues', () => {
|
|
480
|
+
const validator = new QuerySecurityValidator({
|
|
481
|
+
denyValues: {
|
|
482
|
+
'user.role': ['superadmin', 'system']
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
const query = parser.parse('user.role:"superadmin"');
|
|
486
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
487
|
+
expect(() => validator.validate(query)).toThrow(
|
|
488
|
+
'Invalid query parameters'
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should allow same value for different fields when only one is denied', () => {
|
|
493
|
+
const validator = new QuerySecurityValidator({
|
|
494
|
+
denyValues: {
|
|
495
|
+
role: ['admin']
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
// Query for 'type' field with value 'admin' should pass since only 'role' field denies 'admin'
|
|
499
|
+
const query = parser.parse('type:"admin"');
|
|
500
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should handle multiple fields with denyValues', () => {
|
|
504
|
+
const validator = new QuerySecurityValidator({
|
|
505
|
+
denyValues: {
|
|
506
|
+
status: ['deleted', 'banned'],
|
|
507
|
+
role: ['superadmin'],
|
|
508
|
+
priority: [0]
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Should reject when any denied value is used
|
|
513
|
+
const query1 = parser.parse('status:"active" AND role:"superadmin"');
|
|
514
|
+
expect(() => validator.validate(query1)).toThrow(QuerySecurityError);
|
|
515
|
+
|
|
516
|
+
// Should accept when all values are allowed
|
|
517
|
+
const query2 = parser.parse('status:"active" AND role:"admin"');
|
|
518
|
+
expect(() => validator.validate(query2)).not.toThrow();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should work in combination with denyFields', () => {
|
|
522
|
+
const validator = new QuerySecurityValidator({
|
|
523
|
+
denyFields: ['password'],
|
|
524
|
+
denyValues: {
|
|
525
|
+
status: ['deleted']
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Should reject due to denyFields
|
|
530
|
+
const query1 = parser.parse('password:"secret"');
|
|
531
|
+
expect(() => validator.validate(query1)).toThrow(QuerySecurityError);
|
|
532
|
+
|
|
533
|
+
// Should reject due to denyValues
|
|
534
|
+
const query2 = parser.parse('status:"deleted"');
|
|
535
|
+
expect(() => validator.validate(query2)).toThrow(QuerySecurityError);
|
|
536
|
+
|
|
537
|
+
// Should accept when both are satisfied
|
|
538
|
+
const query3 = parser.parse('status:"active" AND username:"john"');
|
|
539
|
+
expect(() => validator.validate(query3)).not.toThrow();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should work in combination with allowedFields', () => {
|
|
543
|
+
const validator = new QuerySecurityValidator({
|
|
544
|
+
allowedFields: ['status', 'name'],
|
|
545
|
+
denyValues: {
|
|
546
|
+
status: ['deleted']
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Should reject due to denyValues
|
|
551
|
+
const query1 = parser.parse('status:"deleted"');
|
|
552
|
+
expect(() => validator.validate(query1)).toThrow(QuerySecurityError);
|
|
553
|
+
|
|
554
|
+
// Should accept when value is allowed
|
|
555
|
+
const query2 = parser.parse('status:"active" AND name:"John"');
|
|
556
|
+
expect(() => validator.validate(query2)).not.toThrow();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should handle string-number type coercion for denied values', () => {
|
|
560
|
+
const validator = new QuerySecurityValidator({
|
|
561
|
+
denyValues: {
|
|
562
|
+
code: [123, 456]
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
// Query with string "123" should match numeric 123 in denyValues
|
|
566
|
+
const query = parser.parse('code:"123"');
|
|
567
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe('validateNoDotNotation (allowDotNotation)', () => {
|
|
572
|
+
it('should allow dot notation by default', () => {
|
|
573
|
+
const validator = new QuerySecurityValidator({});
|
|
574
|
+
const query = parser.parse('user.name:"John"');
|
|
575
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should allow dot notation when explicitly enabled', () => {
|
|
579
|
+
const validator = new QuerySecurityValidator({
|
|
580
|
+
allowDotNotation: true
|
|
581
|
+
});
|
|
582
|
+
const query = parser.parse('user.name:"John" AND user.role:"admin"');
|
|
583
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('should reject dot notation when disabled', () => {
|
|
587
|
+
const validator = new QuerySecurityValidator({
|
|
588
|
+
allowDotNotation: false
|
|
589
|
+
});
|
|
590
|
+
const query = parser.parse('user.name:"John"');
|
|
591
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
592
|
+
expect(() => validator.validate(query)).toThrow(
|
|
593
|
+
'Dot notation is not allowed in field names'
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should include the field name in error message', () => {
|
|
598
|
+
const validator = new QuerySecurityValidator({
|
|
599
|
+
allowDotNotation: false
|
|
600
|
+
});
|
|
601
|
+
const query = parser.parse('metadata.secret:"value"');
|
|
602
|
+
expect(() => validator.validate(query)).toThrow('metadata.secret');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should suggest using simple field names in error message', () => {
|
|
606
|
+
const validator = new QuerySecurityValidator({
|
|
607
|
+
allowDotNotation: false
|
|
608
|
+
});
|
|
609
|
+
const query = parser.parse('user.email:"test@example.com"');
|
|
610
|
+
expect(() => validator.validate(query)).toThrow(
|
|
611
|
+
'use a simple field name without dots'
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('should allow simple field names when dot notation is disabled', () => {
|
|
616
|
+
const validator = new QuerySecurityValidator({
|
|
617
|
+
allowDotNotation: false
|
|
618
|
+
});
|
|
619
|
+
const query = parser.parse('name:"John" AND email:"john@example.com"');
|
|
620
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should reject deeply nested dot notation', () => {
|
|
624
|
+
const validator = new QuerySecurityValidator({
|
|
625
|
+
allowDotNotation: false
|
|
626
|
+
});
|
|
627
|
+
const query = parser.parse('user.profile.settings.theme:"dark"');
|
|
628
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
629
|
+
expect(() => validator.validate(query)).toThrow(
|
|
630
|
+
'user.profile.settings.theme'
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should validate dot notation in nested logical expressions', () => {
|
|
635
|
+
const validator = new QuerySecurityValidator({
|
|
636
|
+
allowDotNotation: false
|
|
637
|
+
});
|
|
638
|
+
const query = parser.parse('name:"John" AND user.role:"admin"');
|
|
639
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
640
|
+
expect(() => validator.validate(query)).toThrow('user.role');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('should validate dot notation in complex nested expressions', () => {
|
|
644
|
+
const validator = new QuerySecurityValidator({
|
|
645
|
+
allowDotNotation: false
|
|
646
|
+
});
|
|
647
|
+
const query = parser.parse(
|
|
648
|
+
'(name:"John" OR name:"Jane") AND (status:"active" OR user.type:"premium")'
|
|
649
|
+
);
|
|
650
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
651
|
+
expect(() => validator.validate(query)).toThrow('user.type');
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should validate dot notation in OR branches', () => {
|
|
655
|
+
const validator = new QuerySecurityValidator({
|
|
656
|
+
allowDotNotation: false
|
|
657
|
+
});
|
|
658
|
+
const query = parser.parse('name:"John" OR metadata.tags:"vip"');
|
|
659
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should work with other security options when dot notation is disabled', () => {
|
|
663
|
+
const validator = new QuerySecurityValidator({
|
|
664
|
+
allowDotNotation: false,
|
|
665
|
+
allowedFields: ['name', 'email', 'status'],
|
|
666
|
+
maxQueryDepth: 3
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Should pass - simple fields, within depth
|
|
670
|
+
const validQuery = parser.parse('name:"John" AND status:"active"');
|
|
671
|
+
expect(() => validator.validate(validQuery)).not.toThrow();
|
|
672
|
+
|
|
673
|
+
// Should fail - dot notation
|
|
674
|
+
const dotQuery = parser.parse('user.name:"John"');
|
|
675
|
+
expect(() => validator.validate(dotQuery)).toThrow(
|
|
676
|
+
'Dot notation is not allowed'
|
|
677
|
+
);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('should reject table-qualified column access when disabled', () => {
|
|
681
|
+
const validator = new QuerySecurityValidator({
|
|
682
|
+
allowDotNotation: false
|
|
683
|
+
});
|
|
684
|
+
// Simulating attempt to access another table's column
|
|
685
|
+
const query = parser.parse('users.password:"secret"');
|
|
686
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
687
|
+
expect(() => validator.validate(query)).toThrow('users.password');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('should reject JSON path access when disabled', () => {
|
|
691
|
+
const validator = new QuerySecurityValidator({
|
|
692
|
+
allowDotNotation: false
|
|
693
|
+
});
|
|
694
|
+
// Simulating attempt to access JSON/JSONB nested field
|
|
695
|
+
const query = parser.parse(
|
|
696
|
+
'config.database.connectionString:"postgres://"'
|
|
697
|
+
);
|
|
698
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
334
702
|
describe('SQL Injection Prevention', () => {
|
|
335
703
|
// Testing protection against common SQL injection patterns
|
|
336
704
|
|
|
@@ -148,9 +148,17 @@ export class QuerySecurityValidator {
|
|
|
148
148
|
expression: QueryExpression,
|
|
149
149
|
schema?: Record<string, Record<string, unknown>>
|
|
150
150
|
): void {
|
|
151
|
+
// Check for dot notation if disabled
|
|
152
|
+
if (!this.options.allowDotNotation) {
|
|
153
|
+
this.validateNoDotNotation(expression);
|
|
154
|
+
}
|
|
155
|
+
|
|
151
156
|
// Check for field restrictions if specified
|
|
152
157
|
this.validateFields(expression, schema);
|
|
153
158
|
|
|
159
|
+
// Check for denied values if specified
|
|
160
|
+
this.validateDenyValues(expression);
|
|
161
|
+
|
|
154
162
|
// Check query complexity
|
|
155
163
|
this.validateQueryDepth(expression, 0);
|
|
156
164
|
this.validateClauseCount(expression);
|
|
@@ -210,6 +218,115 @@ export class QuerySecurityValidator {
|
|
|
210
218
|
}
|
|
211
219
|
}
|
|
212
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Validate that field names do not contain dot notation
|
|
223
|
+
*
|
|
224
|
+
* When allowDotNotation is disabled, this method ensures no field names
|
|
225
|
+
* contain dots, which could be used for:
|
|
226
|
+
* - Table-qualified column access (e.g., "users.password")
|
|
227
|
+
* - Nested JSON/JSONB field access (e.g., "metadata.secret")
|
|
228
|
+
* - Probing internal table structures
|
|
229
|
+
*
|
|
230
|
+
* @private
|
|
231
|
+
* @param expression - The query expression to validate
|
|
232
|
+
* @throws {QuerySecurityError} If a field name contains dot notation
|
|
233
|
+
*/
|
|
234
|
+
private validateNoDotNotation(expression: QueryExpression): void {
|
|
235
|
+
if (expression.type === 'comparison') {
|
|
236
|
+
const { field } = expression;
|
|
237
|
+
if (field.includes('.')) {
|
|
238
|
+
throw new QuerySecurityError(
|
|
239
|
+
`Dot notation is not allowed in field names. ` +
|
|
240
|
+
`Found "${field}" - use a simple field name without dots instead.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
// Recursively validate logical expressions
|
|
245
|
+
this.validateNoDotNotation(expression.left);
|
|
246
|
+
if (expression.right) {
|
|
247
|
+
this.validateNoDotNotation(expression.right);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Validate that query values are not in the denied values list for their field
|
|
254
|
+
*
|
|
255
|
+
* This method checks each comparison expression to ensure the value being
|
|
256
|
+
* queried is not in the denyValues list for that field. This provides
|
|
257
|
+
* granular control over what values can be queried for specific fields.
|
|
258
|
+
*
|
|
259
|
+
* @private
|
|
260
|
+
* @param expression - The query expression to validate
|
|
261
|
+
* @throws {QuerySecurityError} If a denied value is found in the query
|
|
262
|
+
*/
|
|
263
|
+
private validateDenyValues(expression: QueryExpression): void {
|
|
264
|
+
// Skip if no denyValues configured
|
|
265
|
+
if (Object.keys(this.options.denyValues).length === 0) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (expression.type === 'comparison') {
|
|
270
|
+
const { field, value } = expression;
|
|
271
|
+
const deniedValues = this.options.denyValues[field];
|
|
272
|
+
|
|
273
|
+
if (deniedValues && deniedValues.length > 0) {
|
|
274
|
+
// Check if the value is an array (for IN/NOT IN operators)
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
for (const item of value) {
|
|
277
|
+
if (this.isValueDenied(item, deniedValues)) {
|
|
278
|
+
throw new QuerySecurityError('Invalid query parameters');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
// Single value comparison
|
|
283
|
+
if (this.isValueDenied(value, deniedValues)) {
|
|
284
|
+
throw new QuerySecurityError('Invalid query parameters');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// Recursively validate logical expressions
|
|
290
|
+
this.validateDenyValues(expression.left);
|
|
291
|
+
if (expression.right) {
|
|
292
|
+
this.validateDenyValues(expression.right);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Check if a value is in the denied values list
|
|
299
|
+
*
|
|
300
|
+
* @private
|
|
301
|
+
* @param value - The value to check
|
|
302
|
+
* @param deniedValues - The list of denied values
|
|
303
|
+
* @returns true if the value is denied, false otherwise
|
|
304
|
+
*/
|
|
305
|
+
private isValueDenied(
|
|
306
|
+
value: string | number | boolean | null,
|
|
307
|
+
deniedValues: Array<string | number | boolean | null>
|
|
308
|
+
): boolean {
|
|
309
|
+
// Use strict equality to match values, handling type coercion properly
|
|
310
|
+
return deniedValues.some(deniedValue => {
|
|
311
|
+
// Handle null comparison explicitly
|
|
312
|
+
if (value === null && deniedValue === null) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
// Handle same-type comparison with strict equality
|
|
316
|
+
if (typeof value === typeof deniedValue) {
|
|
317
|
+
return value === deniedValue;
|
|
318
|
+
}
|
|
319
|
+
// Handle string/number comparison (common case)
|
|
320
|
+
if (typeof value === 'string' && typeof deniedValue === 'number') {
|
|
321
|
+
return value === String(deniedValue);
|
|
322
|
+
}
|
|
323
|
+
if (typeof value === 'number' && typeof deniedValue === 'string') {
|
|
324
|
+
return String(value) === deniedValue;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
213
330
|
/**
|
|
214
331
|
* Validate that query depth does not exceed the maximum
|
|
215
332
|
*
|