@gblikas/querykit 0.0.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/.cursor/BUGBOT.md +21 -0
- package/.cursor/rules/01-project-structure.mdc +77 -0
- package/.cursor/rules/02-typescript-standards.mdc +105 -0
- package/.cursor/rules/03-testing-standards.mdc +78 -0
- package/.cursor/rules/04-query-language.mdc +79 -0
- package/.cursor/rules/05-solid-principles.mdc +118 -0
- package/.cursor/rules/liqe-readme-docs.mdc +438 -0
- package/.devcontainer/devcontainer.json +25 -0
- package/.eslintignore +1 -0
- package/.eslintrc.js +39 -0
- package/.github/dependabot.yml +12 -0
- package/.github/workflows/ci.yml +114 -0
- package/.github/workflows/publish.yml +61 -0
- package/.husky/pre-commit +30 -0
- package/.prettierrc +10 -0
- package/CONTRIBUTING.md +187 -0
- package/LICENSE +674 -0
- package/README.md +237 -0
- package/dist/adapters/drizzle/index.d.ts +122 -0
- package/dist/adapters/drizzle/index.js +166 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +25 -0
- package/dist/adapters/types.d.ts +60 -0
- package/dist/adapters/types.js +8 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +118 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +18 -0
- package/dist/parser/parser.d.ts +51 -0
- package/dist/parser/parser.js +201 -0
- package/dist/parser/types.d.ts +68 -0
- package/dist/parser/types.js +5 -0
- package/dist/query/builder.d.ts +61 -0
- package/dist/query/builder.js +188 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +18 -0
- package/dist/query/types.d.ts +79 -0
- package/dist/query/types.js +2 -0
- package/dist/security/index.d.ts +2 -0
- package/dist/security/index.js +18 -0
- package/dist/security/types.d.ts +181 -0
- package/dist/security/types.js +43 -0
- package/dist/security/validator.d.ts +191 -0
- package/dist/security/validator.js +344 -0
- package/dist/translators/drizzle/index.d.ts +73 -0
- package/dist/translators/drizzle/index.js +260 -0
- package/dist/translators/index.d.ts +8 -0
- package/dist/translators/index.js +27 -0
- package/dist/translators/sql/index.d.ts +108 -0
- package/dist/translators/sql/index.js +252 -0
- package/dist/translators/types.d.ts +39 -0
- package/dist/translators/types.js +8 -0
- package/examples/qk-next/README.md +35 -0
- package/examples/qk-next/app/favicon.ico +0 -0
- package/examples/qk-next/app/globals.css +122 -0
- package/examples/qk-next/app/layout.tsx +121 -0
- package/examples/qk-next/app/page.tsx +813 -0
- package/examples/qk-next/app/providers.tsx +80 -0
- package/examples/qk-next/components/aurora-background.tsx +12 -0
- package/examples/qk-next/components/github-stars.tsx +51 -0
- package/examples/qk-next/components/mode-toggle.tsx +27 -0
- package/examples/qk-next/components/reactbits/blocks/Backgrounds/Aurora/Aurora.tsx +217 -0
- package/examples/qk-next/components/reactbits/blocks/Backgrounds/LightRays/LightRays.tsx +474 -0
- package/examples/qk-next/components/theme-provider.tsx +11 -0
- package/examples/qk-next/components/ui/card.tsx +92 -0
- package/examples/qk-next/components/ui/command.tsx +184 -0
- package/examples/qk-next/components/ui/dialog.tsx +143 -0
- package/examples/qk-next/components/ui/drawer.tsx +135 -0
- package/examples/qk-next/components/ui/hover-card.tsx +44 -0
- package/examples/qk-next/components/ui/icons.tsx +148 -0
- package/examples/qk-next/components/ui/sonner.tsx +26 -0
- package/examples/qk-next/components/ui/table.tsx +117 -0
- package/examples/qk-next/components.json +21 -0
- package/examples/qk-next/eslint.config.mjs +21 -0
- package/examples/qk-next/jsrepo.json +13 -0
- package/examples/qk-next/lib/utils.ts +6 -0
- package/examples/qk-next/next.config.ts +8 -0
- package/examples/qk-next/package.json +48 -0
- package/examples/qk-next/pnpm-lock.yaml +5558 -0
- package/examples/qk-next/postcss.config.mjs +5 -0
- package/examples/qk-next/public/file.svg +1 -0
- package/examples/qk-next/public/globe.svg +1 -0
- package/examples/qk-next/public/next.svg +1 -0
- package/examples/qk-next/public/vercel.svg +1 -0
- package/examples/qk-next/public/window.svg +1 -0
- package/examples/qk-next/tsconfig.json +42 -0
- package/examples/qk-next/types/sonner.d.ts +3 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/src/adapters/drizzle/drizzle-adapter.test.ts +115 -0
- package/src/adapters/drizzle/index.ts +299 -0
- package/src/adapters/index.ts +11 -0
- package/src/adapters/types.ts +72 -0
- package/src/index.ts +194 -0
- package/src/integration.test.ts +202 -0
- package/src/parser/index.ts +2 -0
- package/src/parser/parser.test.ts +1056 -0
- package/src/parser/parser.ts +268 -0
- package/src/parser/types.ts +97 -0
- package/src/query/builder.test.ts +272 -0
- package/src/query/builder.ts +274 -0
- package/src/query/index.ts +2 -0
- package/src/query/types.ts +107 -0
- package/src/security/index.ts +2 -0
- package/src/security/types.ts +210 -0
- package/src/security/validator.test.ts +459 -0
- package/src/security/validator.ts +395 -0
- package/src/security.test.ts +366 -0
- package/src/translators/drizzle/drizzle-translator.test.ts +128 -0
- package/src/translators/drizzle/index.test.ts +45 -0
- package/src/translators/drizzle/index.ts +346 -0
- package/src/translators/index.ts +14 -0
- package/src/translators/sql/index.test.ts +45 -0
- package/src/translators/sql/index.ts +331 -0
- package/src/translators/sql/sql-translator.test.ts +419 -0
- package/src/translators/types.ts +44 -0
- package/src/types/sonner.d.ts +3 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { QuerySecurityValidator, QuerySecurityError } from './validator';
|
|
2
|
+
import { QueryParser } from '../parser/parser';
|
|
3
|
+
import { QueryExpression, IComparisonExpression } from '../parser/types';
|
|
4
|
+
|
|
5
|
+
describe('QuerySecurityValidator', () => {
|
|
6
|
+
let validator: QuerySecurityValidator;
|
|
7
|
+
let parser: QueryParser;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
parser = new QueryParser();
|
|
11
|
+
validator = new QuerySecurityValidator();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('constructor', () => {
|
|
15
|
+
it('should use default options when none provided', () => {
|
|
16
|
+
const validator = new QuerySecurityValidator();
|
|
17
|
+
expect(validator).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should override default options with provided options', () => {
|
|
21
|
+
const validator = new QuerySecurityValidator({
|
|
22
|
+
maxQueryDepth: 3,
|
|
23
|
+
maxClauseCount: 5
|
|
24
|
+
});
|
|
25
|
+
expect(validator).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('validate', () => {
|
|
30
|
+
it('should not throw error for valid query', () => {
|
|
31
|
+
const query = parser.parse('name:"John" AND priority:>18');
|
|
32
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should validate against a provided schema', () => {
|
|
36
|
+
const query = parser.parse('user.name:"John" AND user.priority:>18');
|
|
37
|
+
const schema = {
|
|
38
|
+
user: {
|
|
39
|
+
name: 'string',
|
|
40
|
+
priority: 'number'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
expect(() => validator.validate(query, schema)).not.toThrow();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should validate field types correctly', () => {
|
|
47
|
+
const query = parser.parse('name:"John" AND priority:>2');
|
|
48
|
+
const schema = {
|
|
49
|
+
user: {
|
|
50
|
+
name: 'string',
|
|
51
|
+
priority: 'number'
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Should pass validation with correct schema
|
|
56
|
+
expect(() => validator.validate(query, schema)).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should validate complex field paths', () => {
|
|
60
|
+
const query = parser.parse('user.name:"John" AND user.priority:>2');
|
|
61
|
+
const schema = {
|
|
62
|
+
user: {
|
|
63
|
+
name: 'string',
|
|
64
|
+
priority: 'number'
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Should pass validation with correct schema
|
|
69
|
+
expect(() => validator.validate(query, schema)).not.toThrow();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should validate allowed fields correctly', () => {
|
|
73
|
+
const query = parser.parse('name:"John" AND priority:>2');
|
|
74
|
+
const validatorWithAllowedFields = new QuerySecurityValidator({
|
|
75
|
+
allowedFields: ['name', 'priority', 'status']
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Should not throw when all fields are allowed
|
|
79
|
+
expect(() => validatorWithAllowedFields.validate(query)).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should validate query depth correctly', () => {
|
|
83
|
+
const query = parser.parse(
|
|
84
|
+
'(name:"John" AND priority:>2) AND (role:"admin" OR status:"active")'
|
|
85
|
+
);
|
|
86
|
+
const validatorWithDepthLimit = new QuerySecurityValidator({
|
|
87
|
+
maxQueryDepth: 3
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Should not throw when query depth is within limits
|
|
91
|
+
expect(() => validatorWithDepthLimit.validate(query)).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should validate clause count correctly', () => {
|
|
95
|
+
const query = parser.parse(
|
|
96
|
+
'name:"John" AND priority:>2 AND role:"admin" AND status:"active"'
|
|
97
|
+
);
|
|
98
|
+
const validatorWithClauseLimit = new QuerySecurityValidator({
|
|
99
|
+
maxClauseCount: 4
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Should not throw when clause count is within limits
|
|
103
|
+
expect(() => validatorWithClauseLimit.validate(query)).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should reject queries exceeding clause limit', () => {
|
|
107
|
+
const query = parser.parse(
|
|
108
|
+
'name:"John" AND priority:>2 AND role:"admin" AND status:"active"'
|
|
109
|
+
);
|
|
110
|
+
const validatorWithLowerClauseLimit = new QuerySecurityValidator({
|
|
111
|
+
maxClauseCount: 3
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Should throw when clause count exceeds limit
|
|
115
|
+
expect(() => validatorWithLowerClauseLimit.validate(query)).toThrow(
|
|
116
|
+
QuerySecurityError
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('validateFields', () => {
|
|
122
|
+
it('should allow fields from schema', () => {
|
|
123
|
+
const query = parser.parse('user.name:"John"');
|
|
124
|
+
const schema = {
|
|
125
|
+
user: {
|
|
126
|
+
name: 'string',
|
|
127
|
+
priority: 'number'
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
expect(() => validator.validate(query, schema)).not.toThrow();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should allow fields in allowedFields list', () => {
|
|
134
|
+
const validator = new QuerySecurityValidator({
|
|
135
|
+
allowedFields: ['name', 'priority']
|
|
136
|
+
});
|
|
137
|
+
const query = parser.parse('name:"John" AND priority:>18');
|
|
138
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should reject fields not in allowedFields list', () => {
|
|
142
|
+
const validator = new QuerySecurityValidator({
|
|
143
|
+
allowedFields: ['name']
|
|
144
|
+
});
|
|
145
|
+
const query = parser.parse('name:"John" AND priority:>18');
|
|
146
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
147
|
+
expect(() => validator.validate(query)).toThrow(
|
|
148
|
+
'Invalid query parameters'
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should reject fields in denyFields list', () => {
|
|
153
|
+
const validator = new QuerySecurityValidator({
|
|
154
|
+
denyFields: ['password', 'secretKey']
|
|
155
|
+
});
|
|
156
|
+
const query = parser.parse('username:"admin" AND password:"secret"');
|
|
157
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
158
|
+
expect(() => validator.validate(query)).toThrow(
|
|
159
|
+
'Invalid query parameters'
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should reject fields in denyFields even if in allowedFields', () => {
|
|
164
|
+
const validator = new QuerySecurityValidator({
|
|
165
|
+
allowedFields: ['username', 'password'],
|
|
166
|
+
denyFields: ['password']
|
|
167
|
+
});
|
|
168
|
+
const query = parser.parse('username:"admin" AND password:"secret"');
|
|
169
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
170
|
+
expect(() => validator.validate(query)).toThrow(
|
|
171
|
+
'Invalid query parameters'
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('validateQueryDepth', () => {
|
|
177
|
+
it('should accept queries within depth limit', () => {
|
|
178
|
+
const validator = new QuerySecurityValidator({
|
|
179
|
+
maxQueryDepth: 3
|
|
180
|
+
});
|
|
181
|
+
// Depth = 3: (a && b) && (c || d)
|
|
182
|
+
const query = parser.parse(
|
|
183
|
+
'(name:"John" AND priority:>18) AND (role:"admin" OR status:"active")'
|
|
184
|
+
);
|
|
185
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should reject queries exceeding depth limit', () => {
|
|
189
|
+
// Create a mock query with excessive depth
|
|
190
|
+
const deepQuery: QueryExpression = {
|
|
191
|
+
type: 'logical',
|
|
192
|
+
operator: 'AND',
|
|
193
|
+
left: {
|
|
194
|
+
type: 'logical',
|
|
195
|
+
operator: 'AND',
|
|
196
|
+
left: {
|
|
197
|
+
type: 'logical',
|
|
198
|
+
operator: 'AND',
|
|
199
|
+
left: {
|
|
200
|
+
type: 'comparison',
|
|
201
|
+
field: 'level1',
|
|
202
|
+
operator: '==',
|
|
203
|
+
value: 'value1'
|
|
204
|
+
},
|
|
205
|
+
right: {
|
|
206
|
+
type: 'comparison',
|
|
207
|
+
field: 'level2',
|
|
208
|
+
operator: '==',
|
|
209
|
+
value: 'value2'
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
right: {
|
|
213
|
+
type: 'comparison',
|
|
214
|
+
field: 'level3',
|
|
215
|
+
operator: '==',
|
|
216
|
+
value: 'value3'
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
right: {
|
|
220
|
+
type: 'comparison',
|
|
221
|
+
field: 'level4',
|
|
222
|
+
operator: '==',
|
|
223
|
+
value: 'value4'
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const testValidator = new QuerySecurityValidator({
|
|
228
|
+
maxQueryDepth: 2
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(() => testValidator.validate(deepQuery)).toThrow(
|
|
232
|
+
QuerySecurityError
|
|
233
|
+
);
|
|
234
|
+
expect(() => testValidator.validate(deepQuery)).toThrow(
|
|
235
|
+
'Query exceeds maximum depth of 2'
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('validateClauseCount', () => {
|
|
241
|
+
it('should accept queries within clause count limit', () => {
|
|
242
|
+
const validator = new QuerySecurityValidator({
|
|
243
|
+
maxClauseCount: 4
|
|
244
|
+
});
|
|
245
|
+
// 4 clauses: a, b, c, d
|
|
246
|
+
const query = parser.parse(
|
|
247
|
+
'name:"John" AND priority:>18 AND role:"admin" AND status:"active"'
|
|
248
|
+
);
|
|
249
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should reject queries exceeding clause count limit', () => {
|
|
253
|
+
const validator = new QuerySecurityValidator({
|
|
254
|
+
maxClauseCount: 3
|
|
255
|
+
});
|
|
256
|
+
// 4 clauses: a, b, c, d
|
|
257
|
+
const query = parser.parse(
|
|
258
|
+
'name:"John" AND priority:>18 AND role:"admin" AND status:"active"'
|
|
259
|
+
);
|
|
260
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
261
|
+
expect(() => validator.validate(query)).toThrow(
|
|
262
|
+
/Query exceeds maximum clause count of 3/
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('validateValueLengths', () => {
|
|
268
|
+
it('should accept string values within length limit', () => {
|
|
269
|
+
const validator = new QuerySecurityValidator({
|
|
270
|
+
maxValueLength: 10
|
|
271
|
+
});
|
|
272
|
+
const query = parser.parse('name:"John"');
|
|
273
|
+
expect(() => validator.validate(query)).not.toThrow();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should reject string values exceeding length limit', () => {
|
|
277
|
+
const validator = new QuerySecurityValidator({
|
|
278
|
+
maxValueLength: 5
|
|
279
|
+
});
|
|
280
|
+
const query = parser.parse('name:"JohnDoe"'); // Length = 7
|
|
281
|
+
expect(() => validator.validate(query)).toThrow(QuerySecurityError);
|
|
282
|
+
expect(() => validator.validate(query)).toThrow(
|
|
283
|
+
/exceeds maximum length of 5 characters/
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should reject string array values exceeding length limit', () => {
|
|
288
|
+
const validator = new QuerySecurityValidator({
|
|
289
|
+
maxValueLength: 5
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Create a mock expression with an array containing a long string
|
|
293
|
+
const mockQuery: QueryExpression = {
|
|
294
|
+
type: 'comparison',
|
|
295
|
+
field: 'name',
|
|
296
|
+
operator: 'IN',
|
|
297
|
+
value: ['John', 'VeryLongName'] // Second value exceeds maxValueLength
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
301
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
302
|
+
/exceeds maximum length of 5 characters/
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('sanitizeWildcards', () => {
|
|
308
|
+
it('should sanitize excessive wildcards in LIKE queries', () => {
|
|
309
|
+
const validator = new QuerySecurityValidator({
|
|
310
|
+
sanitizeWildcards: true
|
|
311
|
+
});
|
|
312
|
+
const expression = parser.parse('name:"a*****b"') as QueryExpression;
|
|
313
|
+
validator.validate(expression);
|
|
314
|
+
|
|
315
|
+
// Expectation: expression has been modified to sanitize wildcards
|
|
316
|
+
// Since this is modifying the expression in-place, we use a type assertion
|
|
317
|
+
const comparisonExpr = expression as IComparisonExpression;
|
|
318
|
+
expect(comparisonExpr.value).toBe('a*b');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should not sanitize wildcards when the option is disabled', () => {
|
|
322
|
+
const validator = new QuerySecurityValidator({
|
|
323
|
+
sanitizeWildcards: false
|
|
324
|
+
});
|
|
325
|
+
const expression = parser.parse('name:"a*****b"') as QueryExpression;
|
|
326
|
+
validator.validate(expression);
|
|
327
|
+
|
|
328
|
+
// Expectation: expression has not been modified
|
|
329
|
+
const comparisonExpr = expression as IComparisonExpression;
|
|
330
|
+
expect(comparisonExpr.value).toBe('a*****b');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('SQL Injection Prevention', () => {
|
|
335
|
+
// Testing protection against common SQL injection patterns
|
|
336
|
+
|
|
337
|
+
// Union-based injection tests
|
|
338
|
+
it('should protect against UNION-based SQL injection', () => {
|
|
339
|
+
const validator = new QuerySecurityValidator({
|
|
340
|
+
allowedFields: ['username'], // Restrict to just username field
|
|
341
|
+
maxValueLength: 100 // Allow long enough for the test
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Create a mock query that would be a UNION-based injection if executed
|
|
345
|
+
const mockQuery: QueryExpression = {
|
|
346
|
+
type: 'comparison',
|
|
347
|
+
field: 'malicious_field', // This field isn't in allowedFields
|
|
348
|
+
operator: '==',
|
|
349
|
+
value: "admin' UNION SELECT username,password,NULL FROM users;--"
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
353
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
354
|
+
'Invalid query parameters'
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Error-based injection test
|
|
359
|
+
it('should protect against error-based SQL injection', () => {
|
|
360
|
+
const validator = new QuerySecurityValidator({
|
|
361
|
+
allowedFields: ['id'], // Restrict fields
|
|
362
|
+
maxValueLength: 100
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Create a mock query that would be an error-based injection
|
|
366
|
+
const mockQuery: QueryExpression = {
|
|
367
|
+
type: 'comparison',
|
|
368
|
+
field: 'malicious_field', // Not in allowedFields
|
|
369
|
+
operator: '==',
|
|
370
|
+
value:
|
|
371
|
+
"1' OR (SELECT CASE WHEN (username='admin' AND SUBSTRING(password,1,1)='a') THEN 1/0 ELSE 'a' END FROM users WHERE id=1)--"
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Boolean-based injection test
|
|
378
|
+
it('should protect against boolean-based SQL injection attempts', () => {
|
|
379
|
+
const validator = new QuerySecurityValidator({
|
|
380
|
+
maxValueLength: 20 // Set a lower limit for test
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Create a mock query with a long value that would exceed maxValueLength
|
|
384
|
+
const mockQuery: QueryExpression = {
|
|
385
|
+
type: 'comparison',
|
|
386
|
+
field: 'username',
|
|
387
|
+
operator: '==',
|
|
388
|
+
value:
|
|
389
|
+
"admin' AND (SELECT 1 FROM users WHERE username='admin' AND password LIKE 'a%')=1--"
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
393
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
394
|
+
/exceeds maximum length of 20 characters/
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Time-based injection test
|
|
399
|
+
it('should protect against time-based SQL injection attempts', () => {
|
|
400
|
+
const validator = new QuerySecurityValidator({
|
|
401
|
+
maxValueLength: 30 // Set a reasonable limit
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Create a mock query with a long value that would exceed maxValueLength
|
|
405
|
+
const mockQuery: QueryExpression = {
|
|
406
|
+
type: 'comparison',
|
|
407
|
+
field: 'username',
|
|
408
|
+
operator: '==',
|
|
409
|
+
value:
|
|
410
|
+
"admin' AND IF((SELECT password FROM users WHERE username='admin')='password', SLEEP(5), 0)--"
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
expect(() => validator.validate(mockQuery)).toThrow(QuerySecurityError);
|
|
414
|
+
expect(() => validator.validate(mockQuery)).toThrow(
|
|
415
|
+
/exceeds maximum length of 30 characters/
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Testing against dangerous characters in input
|
|
420
|
+
it('should protect against dangerous SQL characters', () => {
|
|
421
|
+
const validator = new QuerySecurityValidator({
|
|
422
|
+
maxValueLength: 20 // Reduced to ensure at least some values exceed it
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// We'll test each pattern individually to identify which ones work
|
|
426
|
+
const dangerousValues = [
|
|
427
|
+
"admin'; DROP TABLE users;--", // 25 chars
|
|
428
|
+
"admin' OR '1'='1; DELETE FROM users", // 35 chars
|
|
429
|
+
"admin' --",
|
|
430
|
+
'admin/**/OR/**/1=1',
|
|
431
|
+
"admin' WAITFOR DELAY '0:0:5'--" // 31 chars
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
// Test each value individually (at least one should fail)
|
|
435
|
+
let atLeastOneFailed = false;
|
|
436
|
+
|
|
437
|
+
dangerousValues.forEach(value => {
|
|
438
|
+
// Create a vulnerable mock query
|
|
439
|
+
const mockQuery: QueryExpression = {
|
|
440
|
+
type: 'comparison',
|
|
441
|
+
field: 'username', // Use a valid field to focus on value validation
|
|
442
|
+
operator: '==',
|
|
443
|
+
value
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
validator.validate(mockQuery);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error instanceof QuerySecurityError) {
|
|
450
|
+
atLeastOneFailed = true;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Ensure at least one pattern triggered security validation
|
|
456
|
+
expect(atLeastOneFailed).toBe(true);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|