@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,191 @@
|
|
|
1
|
+
import { QueryExpression } from '../parser/types';
|
|
2
|
+
import { ISecurityOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when a query violates security constraints
|
|
5
|
+
*
|
|
6
|
+
* This error is thrown when a query attempts to bypass security settings
|
|
7
|
+
* such as accessing unauthorized fields, exceeding complexity limits,
|
|
8
|
+
* or using potentially dangerous patterns.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* try {
|
|
13
|
+
* queryValidator.validate(parsedQuery);
|
|
14
|
+
* } catch (error) {
|
|
15
|
+
* if (error instanceof QuerySecurityError) {
|
|
16
|
+
* console.error('Security violation:', error.message);
|
|
17
|
+
* // Return appropriate error response to client
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class QuerySecurityError extends Error {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates query expressions against security constraints
|
|
27
|
+
*
|
|
28
|
+
* The QuerySecurityValidator ensures that queries comply with the security
|
|
29
|
+
* rules defined in the ISecurityOptions. It should be used before executing
|
|
30
|
+
* any query to prevent potential security issues or resource exhaustion.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { QuerySecurityValidator, ISecurityOptions } from 'querykit';
|
|
35
|
+
* import { parseQuery } from 'querykit/parser';
|
|
36
|
+
*
|
|
37
|
+
* // Define security options
|
|
38
|
+
* const securityOptions: ISecurityOptions = {
|
|
39
|
+
* allowedFields: ['id', 'name', 'createdAt'],
|
|
40
|
+
* denyFields: ['password'],
|
|
41
|
+
* maxQueryDepth: 5
|
|
42
|
+
* };
|
|
43
|
+
*
|
|
44
|
+
* // Create validator
|
|
45
|
+
* const validator = new QuerySecurityValidator(securityOptions);
|
|
46
|
+
*
|
|
47
|
+
* // Use in API endpoint
|
|
48
|
+
* app.get('/users', (req, res) => {
|
|
49
|
+
* try {
|
|
50
|
+
* // Parse the query from request
|
|
51
|
+
* const queryStr = req.query.filter || '';
|
|
52
|
+
* const parsedQuery = parseQuery(queryStr);
|
|
53
|
+
*
|
|
54
|
+
* // Validate against security rules
|
|
55
|
+
* validator.validate(parsedQuery, userSchema);
|
|
56
|
+
*
|
|
57
|
+
* // If validation passes, execute the query
|
|
58
|
+
* const results = executeQuery(parsedQuery);
|
|
59
|
+
* res.json(results);
|
|
60
|
+
* } catch (error) {
|
|
61
|
+
* if (error instanceof QuerySecurityError) {
|
|
62
|
+
* res.status(400).json({ error: error.message });
|
|
63
|
+
* } else {
|
|
64
|
+
* res.status(500).json({ error: 'Server error' });
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare class QuerySecurityValidator {
|
|
71
|
+
private options;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a new QuerySecurityValidator instance
|
|
74
|
+
*
|
|
75
|
+
* @param options - Security options to apply. If not provided, default options will be used.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* // Create with default security settings
|
|
80
|
+
* const defaultValidator = new QuerySecurityValidator();
|
|
81
|
+
*
|
|
82
|
+
* // Create with custom security settings
|
|
83
|
+
* const strictValidator = new QuerySecurityValidator({
|
|
84
|
+
* maxQueryDepth: 3,
|
|
85
|
+
* maxClauseCount: 10,
|
|
86
|
+
* maxValueLength: 500
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
constructor(options?: ISecurityOptions);
|
|
91
|
+
/**
|
|
92
|
+
* Validate a query expression against security constraints
|
|
93
|
+
*
|
|
94
|
+
* This is the main method that should be called before executing any query.
|
|
95
|
+
* It performs all security checks defined in the options.
|
|
96
|
+
*
|
|
97
|
+
* @param expression - The parsed query expression to validate
|
|
98
|
+
* @param schema - Optional schema definition to validate fields against
|
|
99
|
+
* @throws {QuerySecurityError} If the query violates any security constraints
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* import { parseQuery } from 'querykit/parser';
|
|
104
|
+
*
|
|
105
|
+
* const validator = new QuerySecurityValidator();
|
|
106
|
+
*
|
|
107
|
+
* // Simple validation
|
|
108
|
+
* try {
|
|
109
|
+
* const query = parseQuery('user.name == "John" && user.priority > 2');
|
|
110
|
+
* validator.validate(query);
|
|
111
|
+
* // Query is safe to execute
|
|
112
|
+
* } catch (error) {
|
|
113
|
+
* // Handle security violation
|
|
114
|
+
* }
|
|
115
|
+
*
|
|
116
|
+
* // Validation with schema
|
|
117
|
+
* const userSchema = {
|
|
118
|
+
* user: {
|
|
119
|
+
* id: 'number',
|
|
120
|
+
* name: 'string',
|
|
121
|
+
* priority: 'number',
|
|
122
|
+
* email: 'string'
|
|
123
|
+
* }
|
|
124
|
+
* };
|
|
125
|
+
*
|
|
126
|
+
* try {
|
|
127
|
+
* const query = parseQuery('user.email == "john@example.com"');
|
|
128
|
+
* validator.validate(query, userSchema);
|
|
129
|
+
* // Query is safe to execute
|
|
130
|
+
* } catch (error) {
|
|
131
|
+
* // Handle security violation
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
validate(expression: QueryExpression, schema?: Record<string, Record<string, unknown>>): void;
|
|
136
|
+
/**
|
|
137
|
+
* Validate that query fields are allowed and not denied
|
|
138
|
+
*
|
|
139
|
+
* @private
|
|
140
|
+
* @param expression - The query expression to validate
|
|
141
|
+
* @param schema - Optional schema definition to validate fields against
|
|
142
|
+
*/
|
|
143
|
+
private validateFields;
|
|
144
|
+
/**
|
|
145
|
+
* Validate that query depth does not exceed the maximum
|
|
146
|
+
*
|
|
147
|
+
* @private
|
|
148
|
+
* @param expression - The query expression to validate
|
|
149
|
+
* @param currentDepth - The current depth level in the recursion
|
|
150
|
+
*/
|
|
151
|
+
private validateQueryDepth;
|
|
152
|
+
/**
|
|
153
|
+
* Validate that the number of clauses does not exceed the maximum
|
|
154
|
+
*
|
|
155
|
+
* @private
|
|
156
|
+
* @param expression - The query expression to validate
|
|
157
|
+
*/
|
|
158
|
+
private validateClauseCount;
|
|
159
|
+
/**
|
|
160
|
+
* Count the number of clauses in a query expression
|
|
161
|
+
*
|
|
162
|
+
* @private
|
|
163
|
+
* @param expression - The query expression to count clauses in
|
|
164
|
+
* @returns The total number of comparison clauses
|
|
165
|
+
*/
|
|
166
|
+
private countClauses;
|
|
167
|
+
/**
|
|
168
|
+
* Validate that string values do not exceed maximum length
|
|
169
|
+
* Security: Enhanced to prevent type confusion attacks via arrays/objects
|
|
170
|
+
*
|
|
171
|
+
* @private
|
|
172
|
+
* @param expression - The query expression to validate
|
|
173
|
+
*/
|
|
174
|
+
private validateValueLengths;
|
|
175
|
+
/**
|
|
176
|
+
* Sanitize wildcard patterns in LIKE queries to prevent regex DoS
|
|
177
|
+
* Security: Enhanced to prevent ReDoS attacks via catastrophic backtracking
|
|
178
|
+
*
|
|
179
|
+
* @private
|
|
180
|
+
* @param expression - The query expression to sanitize
|
|
181
|
+
*/
|
|
182
|
+
private sanitizeWildcards;
|
|
183
|
+
/**
|
|
184
|
+
* Collect all field names used in the query
|
|
185
|
+
*
|
|
186
|
+
* @private
|
|
187
|
+
* @param expression - The query expression to collect fields from
|
|
188
|
+
* @param fieldSet - Set to store the collected field names
|
|
189
|
+
*/
|
|
190
|
+
private collectFields;
|
|
191
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuerySecurityValidator = exports.QuerySecurityError = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown when a query violates security constraints
|
|
7
|
+
*
|
|
8
|
+
* This error is thrown when a query attempts to bypass security settings
|
|
9
|
+
* such as accessing unauthorized fields, exceeding complexity limits,
|
|
10
|
+
* or using potentially dangerous patterns.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* try {
|
|
15
|
+
* queryValidator.validate(parsedQuery);
|
|
16
|
+
* } catch (error) {
|
|
17
|
+
* if (error instanceof QuerySecurityError) {
|
|
18
|
+
* console.error('Security violation:', error.message);
|
|
19
|
+
* // Return appropriate error response to client
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
class QuerySecurityError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'QuerySecurityError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.QuerySecurityError = QuerySecurityError;
|
|
31
|
+
/**
|
|
32
|
+
* Validates query expressions against security constraints
|
|
33
|
+
*
|
|
34
|
+
* The QuerySecurityValidator ensures that queries comply with the security
|
|
35
|
+
* rules defined in the ISecurityOptions. It should be used before executing
|
|
36
|
+
* any query to prevent potential security issues or resource exhaustion.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { QuerySecurityValidator, ISecurityOptions } from 'querykit';
|
|
41
|
+
* import { parseQuery } from 'querykit/parser';
|
|
42
|
+
*
|
|
43
|
+
* // Define security options
|
|
44
|
+
* const securityOptions: ISecurityOptions = {
|
|
45
|
+
* allowedFields: ['id', 'name', 'createdAt'],
|
|
46
|
+
* denyFields: ['password'],
|
|
47
|
+
* maxQueryDepth: 5
|
|
48
|
+
* };
|
|
49
|
+
*
|
|
50
|
+
* // Create validator
|
|
51
|
+
* const validator = new QuerySecurityValidator(securityOptions);
|
|
52
|
+
*
|
|
53
|
+
* // Use in API endpoint
|
|
54
|
+
* app.get('/users', (req, res) => {
|
|
55
|
+
* try {
|
|
56
|
+
* // Parse the query from request
|
|
57
|
+
* const queryStr = req.query.filter || '';
|
|
58
|
+
* const parsedQuery = parseQuery(queryStr);
|
|
59
|
+
*
|
|
60
|
+
* // Validate against security rules
|
|
61
|
+
* validator.validate(parsedQuery, userSchema);
|
|
62
|
+
*
|
|
63
|
+
* // If validation passes, execute the query
|
|
64
|
+
* const results = executeQuery(parsedQuery);
|
|
65
|
+
* res.json(results);
|
|
66
|
+
* } catch (error) {
|
|
67
|
+
* if (error instanceof QuerySecurityError) {
|
|
68
|
+
* res.status(400).json({ error: error.message });
|
|
69
|
+
* } else {
|
|
70
|
+
* res.status(500).json({ error: 'Server error' });
|
|
71
|
+
* }
|
|
72
|
+
* }
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
class QuerySecurityValidator {
|
|
77
|
+
/**
|
|
78
|
+
* Creates a new QuerySecurityValidator instance
|
|
79
|
+
*
|
|
80
|
+
* @param options - Security options to apply. If not provided, default options will be used.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* // Create with default security settings
|
|
85
|
+
* const defaultValidator = new QuerySecurityValidator();
|
|
86
|
+
*
|
|
87
|
+
* // Create with custom security settings
|
|
88
|
+
* const strictValidator = new QuerySecurityValidator({
|
|
89
|
+
* maxQueryDepth: 3,
|
|
90
|
+
* maxClauseCount: 10,
|
|
91
|
+
* maxValueLength: 500
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
constructor(options = {}) {
|
|
96
|
+
this.options = {
|
|
97
|
+
...types_1.DEFAULT_SECURITY_OPTIONS,
|
|
98
|
+
...options
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Validate a query expression against security constraints
|
|
103
|
+
*
|
|
104
|
+
* This is the main method that should be called before executing any query.
|
|
105
|
+
* It performs all security checks defined in the options.
|
|
106
|
+
*
|
|
107
|
+
* @param expression - The parsed query expression to validate
|
|
108
|
+
* @param schema - Optional schema definition to validate fields against
|
|
109
|
+
* @throws {QuerySecurityError} If the query violates any security constraints
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* import { parseQuery } from 'querykit/parser';
|
|
114
|
+
*
|
|
115
|
+
* const validator = new QuerySecurityValidator();
|
|
116
|
+
*
|
|
117
|
+
* // Simple validation
|
|
118
|
+
* try {
|
|
119
|
+
* const query = parseQuery('user.name == "John" && user.priority > 2');
|
|
120
|
+
* validator.validate(query);
|
|
121
|
+
* // Query is safe to execute
|
|
122
|
+
* } catch (error) {
|
|
123
|
+
* // Handle security violation
|
|
124
|
+
* }
|
|
125
|
+
*
|
|
126
|
+
* // Validation with schema
|
|
127
|
+
* const userSchema = {
|
|
128
|
+
* user: {
|
|
129
|
+
* id: 'number',
|
|
130
|
+
* name: 'string',
|
|
131
|
+
* priority: 'number',
|
|
132
|
+
* email: 'string'
|
|
133
|
+
* }
|
|
134
|
+
* };
|
|
135
|
+
*
|
|
136
|
+
* try {
|
|
137
|
+
* const query = parseQuery('user.email == "john@example.com"');
|
|
138
|
+
* validator.validate(query, userSchema);
|
|
139
|
+
* // Query is safe to execute
|
|
140
|
+
* } catch (error) {
|
|
141
|
+
* // Handle security violation
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
validate(expression, schema) {
|
|
146
|
+
// Check for field restrictions if specified
|
|
147
|
+
this.validateFields(expression, schema);
|
|
148
|
+
// Check query complexity
|
|
149
|
+
this.validateQueryDepth(expression, 0);
|
|
150
|
+
this.validateClauseCount(expression);
|
|
151
|
+
// Check value lengths
|
|
152
|
+
this.validateValueLengths(expression);
|
|
153
|
+
// Sanitize wildcard patterns if enabled
|
|
154
|
+
if (this.options.sanitizeWildcards) {
|
|
155
|
+
this.sanitizeWildcards(expression);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Validate that query fields are allowed and not denied
|
|
160
|
+
*
|
|
161
|
+
* @private
|
|
162
|
+
* @param expression - The query expression to validate
|
|
163
|
+
* @param schema - Optional schema definition to validate fields against
|
|
164
|
+
*/
|
|
165
|
+
validateFields(expression, schema) {
|
|
166
|
+
const fieldSet = new Set();
|
|
167
|
+
this.collectFields(expression, fieldSet);
|
|
168
|
+
// Create a set of allowed fields
|
|
169
|
+
const allowedFields = new Set();
|
|
170
|
+
// If allowedFields is empty and schema is provided, use schema fields
|
|
171
|
+
if (this.options.allowedFields.length === 0 && schema) {
|
|
172
|
+
for (const table in schema) {
|
|
173
|
+
if (typeof schema[table] === 'object') {
|
|
174
|
+
for (const field in schema[table]) {
|
|
175
|
+
allowedFields.add(`${table}.${field}`);
|
|
176
|
+
allowedFields.add(field);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.options.allowedFields.forEach(field => allowedFields.add(field));
|
|
183
|
+
}
|
|
184
|
+
// Create a set of denied fields
|
|
185
|
+
const deniedFields = new Set(this.options.denyFields);
|
|
186
|
+
// Check each field in the query
|
|
187
|
+
for (const field of fieldSet) {
|
|
188
|
+
// Security fix: Generic error to prevent field enumeration attacks
|
|
189
|
+
if (deniedFields.has(field) ||
|
|
190
|
+
(allowedFields.size > 0 && !allowedFields.has(field))) {
|
|
191
|
+
throw new QuerySecurityError('Invalid query parameters');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Validate that query depth does not exceed the maximum
|
|
197
|
+
*
|
|
198
|
+
* @private
|
|
199
|
+
* @param expression - The query expression to validate
|
|
200
|
+
* @param currentDepth - The current depth level in the recursion
|
|
201
|
+
*/
|
|
202
|
+
validateQueryDepth(expression, currentDepth) {
|
|
203
|
+
if (currentDepth > this.options.maxQueryDepth) {
|
|
204
|
+
throw new QuerySecurityError(`Query exceeds maximum depth of ${this.options.maxQueryDepth}`);
|
|
205
|
+
}
|
|
206
|
+
if (expression.type === 'logical') {
|
|
207
|
+
this.validateQueryDepth(expression.left, currentDepth + 1);
|
|
208
|
+
if (expression.right) {
|
|
209
|
+
this.validateQueryDepth(expression.right, currentDepth + 1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate that the number of clauses does not exceed the maximum
|
|
215
|
+
*
|
|
216
|
+
* @private
|
|
217
|
+
* @param expression - The query expression to validate
|
|
218
|
+
*/
|
|
219
|
+
validateClauseCount(expression) {
|
|
220
|
+
const count = this.countClauses(expression);
|
|
221
|
+
if (count > this.options.maxClauseCount) {
|
|
222
|
+
throw new QuerySecurityError(`Query exceeds maximum clause count of ${this.options.maxClauseCount} (found ${count})`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Count the number of clauses in a query expression
|
|
227
|
+
*
|
|
228
|
+
* @private
|
|
229
|
+
* @param expression - The query expression to count clauses in
|
|
230
|
+
* @returns The total number of comparison clauses
|
|
231
|
+
*/
|
|
232
|
+
countClauses(expression) {
|
|
233
|
+
if (expression.type === 'comparison') {
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
let count = 0;
|
|
237
|
+
count += this.countClauses(expression.left);
|
|
238
|
+
if (expression.right) {
|
|
239
|
+
count += this.countClauses(expression.right);
|
|
240
|
+
}
|
|
241
|
+
return count;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Validate that string values do not exceed maximum length
|
|
245
|
+
* Security: Enhanced to prevent type confusion attacks via arrays/objects
|
|
246
|
+
*
|
|
247
|
+
* @private
|
|
248
|
+
* @param expression - The query expression to validate
|
|
249
|
+
*/
|
|
250
|
+
validateValueLengths(expression) {
|
|
251
|
+
if (expression.type === 'comparison') {
|
|
252
|
+
const { value } = expression;
|
|
253
|
+
// Check string values
|
|
254
|
+
if (typeof value === 'string' &&
|
|
255
|
+
value.length > this.options.maxValueLength) {
|
|
256
|
+
throw new QuerySecurityError(`Query contains a string value that exceeds maximum length of ${this.options.maxValueLength} characters`);
|
|
257
|
+
}
|
|
258
|
+
// Security fix: Enhanced array validation to prevent bypass
|
|
259
|
+
if (Array.isArray(value)) {
|
|
260
|
+
if (value.length > 100) {
|
|
261
|
+
// Limit array size
|
|
262
|
+
throw new QuerySecurityError('Array values cannot exceed 100 items');
|
|
263
|
+
}
|
|
264
|
+
for (const item of value) {
|
|
265
|
+
if (typeof item === 'string' &&
|
|
266
|
+
item.length > this.options.maxValueLength) {
|
|
267
|
+
throw new QuerySecurityError(`Query contains a string value in array that exceeds maximum length of ${this.options.maxValueLength} characters`);
|
|
268
|
+
}
|
|
269
|
+
// Security fix: Prevent object injection in arrays
|
|
270
|
+
if (typeof item === 'object' && item !== null) {
|
|
271
|
+
throw new QuerySecurityError('Object values are not allowed in arrays');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Security fix: Prevent object values entirely
|
|
276
|
+
if (typeof value === 'object' &&
|
|
277
|
+
value !== null &&
|
|
278
|
+
!Array.isArray(value)) {
|
|
279
|
+
throw new QuerySecurityError('Object values are not allowed');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
this.validateValueLengths(expression.left);
|
|
284
|
+
if (expression.right) {
|
|
285
|
+
this.validateValueLengths(expression.right);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Sanitize wildcard patterns in LIKE queries to prevent regex DoS
|
|
291
|
+
* Security: Enhanced to prevent ReDoS attacks via catastrophic backtracking
|
|
292
|
+
*
|
|
293
|
+
* @private
|
|
294
|
+
* @param expression - The query expression to sanitize
|
|
295
|
+
*/
|
|
296
|
+
sanitizeWildcards(expression) {
|
|
297
|
+
if (expression.type === 'comparison') {
|
|
298
|
+
const { operator, value } = expression;
|
|
299
|
+
// Only sanitize LIKE operators with string values
|
|
300
|
+
if (operator === 'LIKE' && typeof value === 'string') {
|
|
301
|
+
// Security fix: Count wildcards to prevent ReDoS
|
|
302
|
+
const wildcardCount = (value.match(/[*?]/g) || []).length;
|
|
303
|
+
if (wildcardCount > 10) {
|
|
304
|
+
throw new QuerySecurityError('Excessive wildcard usage');
|
|
305
|
+
}
|
|
306
|
+
// Security fix: Prevent alternating patterns that cause catastrophic backtracking
|
|
307
|
+
// Pattern like "*a*b*c*d*e*f" (alternating * and non-* chars)
|
|
308
|
+
if (/(\*[^*]+){5,}/.test(value)) {
|
|
309
|
+
throw new QuerySecurityError('Complex wildcard patterns not allowed');
|
|
310
|
+
}
|
|
311
|
+
// Enhanced sanitization: limit consecutive wildcards
|
|
312
|
+
const sanitized = value
|
|
313
|
+
.replace(/\*{2,}/g, '*') // Limit consecutive asterisks
|
|
314
|
+
.replace(/\?{2,}/g, '?'); // Limit consecutive question marks
|
|
315
|
+
expression.value = sanitized;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
this.sanitizeWildcards(expression.left);
|
|
320
|
+
if (expression.right) {
|
|
321
|
+
this.sanitizeWildcards(expression.right);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Collect all field names used in the query
|
|
327
|
+
*
|
|
328
|
+
* @private
|
|
329
|
+
* @param expression - The query expression to collect fields from
|
|
330
|
+
* @param fieldSet - Set to store the collected field names
|
|
331
|
+
*/
|
|
332
|
+
collectFields(expression, fieldSet) {
|
|
333
|
+
if (expression.type === 'comparison') {
|
|
334
|
+
fieldSet.add(expression.field);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
this.collectFields(expression.left, fieldSet);
|
|
338
|
+
if (expression.right) {
|
|
339
|
+
this.collectFields(expression.right, fieldSet);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
exports.QuerySecurityValidator = QuerySecurityValidator;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle ORM Translator for QueryKit
|
|
3
|
+
*
|
|
4
|
+
* This translator converts QueryKit AST expressions into Drizzle ORM
|
|
5
|
+
* query conditions that can be used in Drizzle's SQL query builder.
|
|
6
|
+
*/
|
|
7
|
+
import { SQL } from 'drizzle-orm';
|
|
8
|
+
import { QueryExpression } from '../../parser/types';
|
|
9
|
+
import { ITranslator, ITranslatorOptions } from '../types';
|
|
10
|
+
/**
|
|
11
|
+
* Options specific to the Drizzle translator
|
|
12
|
+
*/
|
|
13
|
+
export interface IDrizzleTranslatorOptions extends ITranslatorOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Schema information for type safety
|
|
16
|
+
*/
|
|
17
|
+
schema?: Record<string, Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Error thrown when translation fails
|
|
21
|
+
*/
|
|
22
|
+
export declare class DrizzleTranslationError extends Error {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Translates QueryKit AST expressions to Drizzle ORM conditions
|
|
27
|
+
*/
|
|
28
|
+
export declare class DrizzleTranslator implements ITranslator<SQL> {
|
|
29
|
+
private options;
|
|
30
|
+
constructor(options?: IDrizzleTranslatorOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Translate a QueryKit expression to a Drizzle ORM condition
|
|
33
|
+
*/
|
|
34
|
+
translate(expression: QueryExpression): SQL;
|
|
35
|
+
/**
|
|
36
|
+
* Check if an expression can be translated to Drizzle ORM
|
|
37
|
+
*/
|
|
38
|
+
canTranslate(expression: QueryExpression): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Translate any QueryKit expression to a Drizzle ORM condition
|
|
41
|
+
*/
|
|
42
|
+
private translateExpression;
|
|
43
|
+
/**
|
|
44
|
+
* Translate a comparison expression to a Drizzle ORM condition
|
|
45
|
+
*/
|
|
46
|
+
private translateComparisonExpression;
|
|
47
|
+
/**
|
|
48
|
+
* Build SQL for a specific operator with raw field name
|
|
49
|
+
* Security: Validates field names to prevent SQL injection
|
|
50
|
+
*/
|
|
51
|
+
private buildSqlForOperator;
|
|
52
|
+
/**
|
|
53
|
+
* Convert wildcard pattern to SQL LIKE pattern
|
|
54
|
+
*/
|
|
55
|
+
private wildcardToSqlPattern;
|
|
56
|
+
/**
|
|
57
|
+
* Translate a logical expression to a Drizzle ORM condition
|
|
58
|
+
*/
|
|
59
|
+
private translateLogicalExpression;
|
|
60
|
+
/**
|
|
61
|
+
* Normalize a field name based on translator options
|
|
62
|
+
*/
|
|
63
|
+
private normalizeField;
|
|
64
|
+
/**
|
|
65
|
+
* Get a field from the schema if it exists
|
|
66
|
+
*/
|
|
67
|
+
private getSchemaField;
|
|
68
|
+
/**
|
|
69
|
+
* Security: Validates field names to prevent SQL injection
|
|
70
|
+
* Only allows alphanumeric chars, dots, underscores. Max 64 chars per part.
|
|
71
|
+
*/
|
|
72
|
+
private isValidFieldName;
|
|
73
|
+
}
|