@alliance-droid/svelte-auth-core 1.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/dist/adapter-context.d.ts +19 -0
- package/dist/adapter-context.d.ts.map +1 -0
- package/dist/adapter-context.js +68 -0
- package/dist/adapter-context.js.map +1 -0
- package/dist/adapters/__tests__/adapter-tests.d.ts +7 -0
- package/dist/adapters/__tests__/adapter-tests.d.ts.map +1 -0
- package/dist/adapters/__tests__/adapter-tests.js +206 -0
- package/dist/adapters/__tests__/adapter-tests.js.map +1 -0
- package/dist/adapters/adapter.d.ts +60 -0
- package/dist/adapters/adapter.d.ts.map +1 -0
- package/dist/adapters/adapter.js +2 -0
- package/dist/adapters/adapter.js.map +1 -0
- package/dist/adapters/filesystem-adapter.d.ts +26 -0
- package/dist/adapters/filesystem-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem-adapter.js +148 -0
- package/dist/adapters/filesystem-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mongodb-adapter.d.ts +27 -0
- package/dist/adapters/mongodb-adapter.d.ts.map +1 -0
- package/dist/adapters/mongodb-adapter.js +213 -0
- package/dist/adapters/mongodb-adapter.js.map +1 -0
- package/dist/adapters/postgres-adapter.d.ts +30 -0
- package/dist/adapters/postgres-adapter.d.ts.map +1 -0
- package/dist/adapters/postgres-adapter.js +237 -0
- package/dist/adapters/postgres-adapter.js.map +1 -0
- package/dist/adapters/sqlite-adapter.d.ts +26 -0
- package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
- package/dist/adapters/sqlite-adapter.js +261 -0
- package/dist/adapters/sqlite-adapter.js.map +1 -0
- package/dist/auth.d.ts +48 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +205 -0
- package/dist/auth.js.map +1 -0
- package/dist/client-jwt.d.ts +30 -0
- package/dist/client-jwt.d.ts.map +1 -0
- package/dist/client-jwt.js +57 -0
- package/dist/client-jwt.js.map +1 -0
- package/dist/client-store.d.ts +31 -0
- package/dist/client-store.d.ts.map +1 -0
- package/dist/client-store.js +122 -0
- package/dist/client-store.js.map +1 -0
- package/dist/cors.d.ts +48 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +88 -0
- package/dist/cors.js.map +1 -0
- package/dist/csrf.d.ts +57 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +95 -0
- package/dist/csrf.js.map +1 -0
- package/dist/db.d.ts +22 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +43 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/input-validation.d.ts +78 -0
- package/dist/input-validation.d.ts.map +1 -0
- package/dist/input-validation.js +238 -0
- package/dist/input-validation.js.map +1 -0
- package/dist/oauth-callback.d.ts +31 -0
- package/dist/oauth-callback.d.ts.map +1 -0
- package/dist/oauth-callback.js +254 -0
- package/dist/oauth-callback.js.map +1 -0
- package/dist/oauth-providers.d.ts +92 -0
- package/dist/oauth-providers.d.ts.map +1 -0
- package/dist/oauth-providers.js +213 -0
- package/dist/oauth-providers.js.map +1 -0
- package/dist/oauth-types.d.ts +77 -0
- package/dist/oauth-types.d.ts.map +1 -0
- package/dist/oauth-types.js +2 -0
- package/dist/oauth-types.js.map +1 -0
- package/dist/password.d.ts +31 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +54 -0
- package/dist/password.js.map +1 -0
- package/dist/providers/github-oauth.d.ts +58 -0
- package/dist/providers/github-oauth.d.ts.map +1 -0
- package/dist/providers/github-oauth.js +230 -0
- package/dist/providers/github-oauth.js.map +1 -0
- package/dist/providers/google-oauth.d.ts +46 -0
- package/dist/providers/google-oauth.d.ts.map +1 -0
- package/dist/providers/google-oauth.js +177 -0
- package/dist/providers/google-oauth.js.map +1 -0
- package/dist/providers/oidc-oauth.d.ts +85 -0
- package/dist/providers/oidc-oauth.d.ts.map +1 -0
- package/dist/providers/oidc-oauth.js +301 -0
- package/dist/providers/oidc-oauth.js.map +1 -0
- package/dist/rate-limit.d.ts +36 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +88 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/rate-limiting.d.ts +113 -0
- package/dist/rate-limiting.d.ts.map +1 -0
- package/dist/rate-limiting.js +221 -0
- package/dist/rate-limiting.js.map +1 -0
- package/dist/security-headers.d.ts +54 -0
- package/dist/security-headers.d.ts.map +1 -0
- package/dist/security-headers.js +123 -0
- package/dist/security-headers.js.map +1 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +33 -0
- package/dist/session.js.map +1 -0
- package/dist/sql-injection-prevention.d.ts +94 -0
- package/dist/sql-injection-prevention.d.ts.map +1 -0
- package/dist/sql-injection-prevention.js +222 -0
- package/dist/sql-injection-prevention.js.map +1 -0
- package/dist/token.d.ts +22 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +31 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/user.d.ts +33 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +144 -0
- package/dist/user.js.map +1 -0
- package/package.json +48 -0
- package/src/adapter-context.ts +72 -0
- package/src/adapters/__tests__/adapter-tests.ts +254 -0
- package/src/adapters/__tests__/filesystem-adapter.test.ts +48 -0
- package/src/adapters/__tests__/mongodb-adapter.test.ts +64 -0
- package/src/adapters/__tests__/postgres-adapter.test.ts +62 -0
- package/src/adapters/__tests__/sqlite-adapter.test.ts +103 -0
- package/src/adapters/__tests__/test-fs-adapter.json +4 -0
- package/src/adapters/adapter.ts +72 -0
- package/src/adapters/filesystem-adapter.ts +153 -0
- package/src/adapters/index.ts +5 -0
- package/src/adapters/mongodb-adapter.ts +208 -0
- package/src/adapters/postgres-adapter.ts +261 -0
- package/src/adapters/sqlite-adapter.ts +284 -0
- package/src/auth.ts +239 -0
- package/src/client-jwt.test.ts +137 -0
- package/src/client-jwt.ts +67 -0
- package/src/client-store.test.ts +149 -0
- package/src/client-store.ts +144 -0
- package/src/cors.test.ts +175 -0
- package/src/cors.ts +115 -0
- package/src/csrf.test.ts +226 -0
- package/src/csrf.ts +126 -0
- package/src/db.ts +57 -0
- package/src/index.ts +143 -0
- package/src/input-validation.test.ts +347 -0
- package/src/input-validation.ts +307 -0
- package/src/integration.test.ts +322 -0
- package/src/oauth-callback.test.ts +282 -0
- package/src/oauth-callback.ts +323 -0
- package/src/oauth-providers.ts +232 -0
- package/src/oauth-types.ts +82 -0
- package/src/password.test.ts +89 -0
- package/src/password.ts +62 -0
- package/src/providers/github-oauth.test.ts +290 -0
- package/src/providers/github-oauth.ts +226 -0
- package/src/providers/google-oauth.test.ts +240 -0
- package/src/providers/google-oauth.ts +166 -0
- package/src/providers/oidc-oauth.test.ts +367 -0
- package/src/providers/oidc-oauth.ts +302 -0
- package/src/rate-limit.test.ts +308 -0
- package/src/rate-limit.ts +118 -0
- package/src/rate-limiting.test.ts +390 -0
- package/src/rate-limiting.ts +275 -0
- package/src/security-headers.test.ts +242 -0
- package/src/security-headers.ts +160 -0
- package/src/security-penetration.test.ts +705 -0
- package/src/session.ts +42 -0
- package/src/sql-injection-prevention.test.ts +337 -0
- package/src/sql-injection-prevention.ts +272 -0
- package/src/token.test.ts +67 -0
- package/src/token.ts +34 -0
- package/src/types.ts +87 -0
- package/src/user.ts +165 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface SessionConfig {
|
|
6
|
+
maxAge: number; // in seconds
|
|
7
|
+
secure: boolean;
|
|
8
|
+
httpOnly: boolean;
|
|
9
|
+
sameSite: 'strict' | 'lax' | 'none';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const defaultSessionConfig: SessionConfig = {
|
|
13
|
+
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
14
|
+
secure: true,
|
|
15
|
+
httpOnly: true,
|
|
16
|
+
sameSite: 'lax'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function createSessionCookie(sessionId: string, config: SessionConfig = defaultSessionConfig): string {
|
|
20
|
+
const flags: string[] = [];
|
|
21
|
+
flags.push(`Max-Age=${config.maxAge}`);
|
|
22
|
+
flags.push(`SameSite=${config.sameSite}`);
|
|
23
|
+
if (config.secure) flags.push('Secure');
|
|
24
|
+
if (config.httpOnly) flags.push('HttpOnly');
|
|
25
|
+
flags.push('Path=/');
|
|
26
|
+
|
|
27
|
+
return `session=${sessionId}; ${flags.join('; ')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseSessionId(cookieHeader: string | null): string | null {
|
|
31
|
+
if (!cookieHeader) return null;
|
|
32
|
+
|
|
33
|
+
const cookies = cookieHeader.split(';');
|
|
34
|
+
for (const cookie of cookies) {
|
|
35
|
+
const [name, value] = cookie.trim().split('=');
|
|
36
|
+
if (name === 'session') {
|
|
37
|
+
return decodeURIComponent(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isSafePropertyName,
|
|
4
|
+
getSafeProperty,
|
|
5
|
+
safeFilter,
|
|
6
|
+
safeSearch,
|
|
7
|
+
sanitizeForLikeQuery,
|
|
8
|
+
prepareDatabaseValue,
|
|
9
|
+
validateDatabaseQuery,
|
|
10
|
+
databaseSecurityGuides,
|
|
11
|
+
getParameterizedQueryExample
|
|
12
|
+
} from './sql-injection-prevention';
|
|
13
|
+
|
|
14
|
+
describe('SQL Injection Prevention', () => {
|
|
15
|
+
describe('isSafePropertyName', () => {
|
|
16
|
+
it('should allow alphanumeric names', () => {
|
|
17
|
+
expect(isSafePropertyName('name')).toBe(true);
|
|
18
|
+
expect(isSafePropertyName('Name123')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should allow underscores and hyphens', () => {
|
|
22
|
+
expect(isSafePropertyName('user_name')).toBe(true);
|
|
23
|
+
expect(isSafePropertyName('user-name')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should reject special characters', () => {
|
|
27
|
+
expect(isSafePropertyName('user@name')).toBe(false);
|
|
28
|
+
expect(isSafePropertyName('user.name')).toBe(false);
|
|
29
|
+
expect(isSafePropertyName('user$name')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should reject SQL injection patterns', () => {
|
|
33
|
+
expect(isSafePropertyName("name'; DROP TABLE users--")).toBe(false);
|
|
34
|
+
expect(isSafePropertyName('name" OR "1"="1')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should reject empty string', () => {
|
|
38
|
+
expect(isSafePropertyName('')).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getSafeProperty', () => {
|
|
43
|
+
const obj = {
|
|
44
|
+
user: {
|
|
45
|
+
email: 'test@example.com',
|
|
46
|
+
profile: {
|
|
47
|
+
name: 'John Doe'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
age: 25
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it('should get top-level property', () => {
|
|
54
|
+
expect(getSafeProperty(obj, 'age')).toBe(25);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should get nested property with dot notation', () => {
|
|
58
|
+
expect(getSafeProperty(obj, 'user.email')).toBe('test@example.com');
|
|
59
|
+
expect(getSafeProperty(obj, 'user.profile.name')).toBe('John Doe');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return default for missing property', () => {
|
|
63
|
+
expect(getSafeProperty(obj, 'missing')).toBeUndefined();
|
|
64
|
+
expect(getSafeProperty(obj, 'missing', 'default')).toBe('default');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return default for unsafe property name', () => {
|
|
68
|
+
expect(getSafeProperty(obj, "'; DROP TABLE--")).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle non-object input', () => {
|
|
72
|
+
expect(getSafeProperty(null, 'prop')).toBeUndefined();
|
|
73
|
+
expect(getSafeProperty('string', 'prop')).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle empty path', () => {
|
|
77
|
+
expect(getSafeProperty(obj, '')).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('safeFilter', () => {
|
|
82
|
+
const users = [
|
|
83
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
84
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
85
|
+
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
it('should filter by exact match', () => {
|
|
89
|
+
const result = safeFilter(users, 'name', 'Alice', true);
|
|
90
|
+
expect(result).toHaveLength(1);
|
|
91
|
+
expect(result[0].name).toBe('Alice');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should filter by number property', () => {
|
|
95
|
+
const result = safeFilter(users, 'id', 2, true);
|
|
96
|
+
expect(result).toHaveLength(1);
|
|
97
|
+
expect(result[0].id).toBe(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should filter by substring match', () => {
|
|
101
|
+
const result = safeFilter(users, 'email', 'example', false);
|
|
102
|
+
expect(result).toHaveLength(3);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return empty array for unsafe property', () => {
|
|
106
|
+
const result = safeFilter(users, "'; DROP TABLE--", 'value', true);
|
|
107
|
+
expect(result).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle non-array input', () => {
|
|
111
|
+
const result = safeFilter('not an array' as any, 'prop', 'value', true);
|
|
112
|
+
expect(result).toHaveLength(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should return empty array if no matches', () => {
|
|
116
|
+
const result = safeFilter(users, 'name', 'NonExistent', true);
|
|
117
|
+
expect(result).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('safeSearch', () => {
|
|
122
|
+
const users = [
|
|
123
|
+
{ id: 1, name: 'Alice', status: 'active' },
|
|
124
|
+
{ id: 2, name: 'Bob', status: 'inactive' },
|
|
125
|
+
{ id: 3, name: 'Charlie', status: 'active' }
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
it('should search with single condition', () => {
|
|
129
|
+
const result = safeSearch(users, { name: 'Alice' });
|
|
130
|
+
expect(result).toHaveLength(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should search with multiple conditions (AND logic)', () => {
|
|
134
|
+
const result = safeSearch(users, { status: 'active', name: 'Alice' });
|
|
135
|
+
expect(result).toHaveLength(1);
|
|
136
|
+
expect(result[0].id).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should return empty if any condition fails', () => {
|
|
140
|
+
const result = safeSearch(users, { status: 'active', name: 'Bob' });
|
|
141
|
+
expect(result).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should reject unsafe property names', () => {
|
|
145
|
+
const result = safeSearch(users, { "'; DROP TABLE--": 'value' });
|
|
146
|
+
expect(result).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle non-array input', () => {
|
|
150
|
+
const result = safeSearch('not an array' as any, { name: 'Alice' });
|
|
151
|
+
expect(result).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('sanitizeForLikeQuery', () => {
|
|
156
|
+
it('should escape percent wildcard', () => {
|
|
157
|
+
expect(sanitizeForLikeQuery('50%')).toBe('50\\%');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should escape underscore wildcard', () => {
|
|
161
|
+
expect(sanitizeForLikeQuery('test_value')).toBe('test\\_value');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should escape backslash', () => {
|
|
165
|
+
expect(sanitizeForLikeQuery('path\\to\\file')).toBe('path\\\\to\\\\file');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should escape multiple special characters', () => {
|
|
169
|
+
expect(sanitizeForLikeQuery('50%_\\value')).toBe('50\\%\\_\\\\value');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should handle non-string input', () => {
|
|
173
|
+
expect(sanitizeForLikeQuery(123 as any)).toBe('');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should preserve normal characters', () => {
|
|
177
|
+
expect(sanitizeForLikeQuery('normal text')).toBe('normal text');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('prepareDatabaseValue', () => {
|
|
182
|
+
it('should return null for null/undefined', () => {
|
|
183
|
+
expect(prepareDatabaseValue(null)).toBeNull();
|
|
184
|
+
expect(prepareDatabaseValue(undefined)).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should return strings as-is', () => {
|
|
188
|
+
expect(prepareDatabaseValue('test')).toBe('test');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return numbers as-is', () => {
|
|
192
|
+
expect(prepareDatabaseValue(42)).toBe(42);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should return booleans as-is', () => {
|
|
196
|
+
expect(prepareDatabaseValue(true)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return objects as-is', () => {
|
|
200
|
+
const obj = { key: 'value' };
|
|
201
|
+
expect(prepareDatabaseValue(obj)).toBe(obj);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return arrays as-is', () => {
|
|
205
|
+
const arr = [1, 2, 3];
|
|
206
|
+
expect(prepareDatabaseValue(arr)).toBe(arr);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should convert other types to null', () => {
|
|
210
|
+
expect(prepareDatabaseValue(Symbol('test'))).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('validateDatabaseQuery', () => {
|
|
215
|
+
it('should validate safe query', () => {
|
|
216
|
+
const result = validateDatabaseQuery({
|
|
217
|
+
email: 'test@example.com',
|
|
218
|
+
name: 'John'
|
|
219
|
+
});
|
|
220
|
+
expect(result.valid).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should reject non-object input', () => {
|
|
224
|
+
const result = validateDatabaseQuery('not an object');
|
|
225
|
+
expect(result.valid).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should reject null input', () => {
|
|
229
|
+
const result = validateDatabaseQuery(null);
|
|
230
|
+
expect(result.valid).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should detect unsafe property names', () => {
|
|
234
|
+
const result = validateDatabaseQuery({
|
|
235
|
+
"'; DROP TABLE users--": 'value'
|
|
236
|
+
});
|
|
237
|
+
expect(result.valid).toBe(false);
|
|
238
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should detect SQL injection patterns in values', () => {
|
|
242
|
+
const result = validateDatabaseQuery({
|
|
243
|
+
name: "'; DROP TABLE users--"
|
|
244
|
+
});
|
|
245
|
+
expect(result.valid).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should report multiple errors', () => {
|
|
249
|
+
const result = validateDatabaseQuery({
|
|
250
|
+
"bad-prop'; DROP": 'test',
|
|
251
|
+
comment: '-- injection'
|
|
252
|
+
});
|
|
253
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(1);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('databaseSecurityGuides', () => {
|
|
258
|
+
it('should provide JSON guide', () => {
|
|
259
|
+
expect(databaseSecurityGuides.json.name).toBe('JSON File Storage');
|
|
260
|
+
expect(databaseSecurityGuides.json.practices).toBeInstanceOf(Array);
|
|
261
|
+
expect(databaseSecurityGuides.json.practices.length).toBeGreaterThan(0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should provide SQL guide', () => {
|
|
265
|
+
expect(databaseSecurityGuides.sql.name).toBe('SQL Databases (PostgreSQL, MySQL, etc.)');
|
|
266
|
+
expect(databaseSecurityGuides.sql.practices).toBeInstanceOf(Array);
|
|
267
|
+
expect(databaseSecurityGuides.sql.practices.length).toBeGreaterThan(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('SQL guide should mention parameterized queries', () => {
|
|
271
|
+
const sqlPractices = databaseSecurityGuides.sql.practices.join(' ');
|
|
272
|
+
expect(sqlPractices.toLowerCase()).toContain('parameterized');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('getParameterizedQueryExample', () => {
|
|
277
|
+
it('should return example code', () => {
|
|
278
|
+
const example = getParameterizedQueryExample();
|
|
279
|
+
expect(example).toBeTruthy();
|
|
280
|
+
expect(typeof example).toBe('string');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should show safe example', () => {
|
|
284
|
+
const example = getParameterizedQueryExample();
|
|
285
|
+
expect(example).toContain('$1');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should show unsafe example', () => {
|
|
289
|
+
const example = getParameterizedQueryExample();
|
|
290
|
+
expect(example).toContain('VULNERABLE');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('Integration scenarios', () => {
|
|
295
|
+
it('should prevent property injection in search', () => {
|
|
296
|
+
const users = [{ id: 1, name: 'Alice' }];
|
|
297
|
+
const injectionAttempt = { "'; DROP": 'value' };
|
|
298
|
+
|
|
299
|
+
const result = safeSearch(users, injectionAttempt);
|
|
300
|
+
expect(result).toHaveLength(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should safely filter with user input', () => {
|
|
304
|
+
const items = [
|
|
305
|
+
{ id: 1, description: 'Test Item 50% off' },
|
|
306
|
+
{ id: 2, description: 'Regular Item' }
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const userSearch = '50%';
|
|
310
|
+
const sanitized = sanitizeForLikeQuery(userSearch);
|
|
311
|
+
expect(sanitized).toBe('50\\%');
|
|
312
|
+
|
|
313
|
+
// Filter would use the sanitized value safely
|
|
314
|
+
const results = safeFilter(items, 'description', 'Test Item 50% off', true);
|
|
315
|
+
expect(results).toHaveLength(1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should validate complete query flow', () => {
|
|
319
|
+
const userInput = {
|
|
320
|
+
email: 'test@example.com',
|
|
321
|
+
status: 'active'
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const validation = validateDatabaseQuery(userInput);
|
|
325
|
+
expect(validation.valid).toBe(true);
|
|
326
|
+
|
|
327
|
+
// Proceed with query
|
|
328
|
+
const mockUsers = [
|
|
329
|
+
{ email: 'test@example.com', status: 'active' },
|
|
330
|
+
{ email: 'other@example.com', status: 'inactive' }
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
const results = safeSearch(mockUsers, userInput);
|
|
334
|
+
expect(results).toHaveLength(1);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Injection Prevention
|
|
3
|
+
* Provides utilities for preventing SQL injection attacks
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This project uses JSON file storage, not SQL. These utilities
|
|
6
|
+
* provide guidance and helpers for:
|
|
7
|
+
* 1. Parameterized query patterns for JSON operations
|
|
8
|
+
* 2. Input sanitization for search/filter operations
|
|
9
|
+
* 3. Safe object property access patterns
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Safe query builder for JSON operations
|
|
14
|
+
* Prevents injection by using parameterized patterns
|
|
15
|
+
*/
|
|
16
|
+
export interface SafeQueryParams {
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate that a property name is safe to access
|
|
22
|
+
* Only allows alphanumeric, underscore, and hyphen characters
|
|
23
|
+
* @param property - Property name to validate
|
|
24
|
+
* @returns True if property name is safe
|
|
25
|
+
*/
|
|
26
|
+
export function isSafePropertyName(property: string): boolean {
|
|
27
|
+
// Only allow alphanumeric, underscore, and hyphen
|
|
28
|
+
return /^[a-zA-Z0-9_-]+$/.test(property);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get object property safely without injection risk
|
|
33
|
+
* @param obj - Object to query
|
|
34
|
+
* @param property - Property path (supports dot notation like 'user.email')
|
|
35
|
+
* @param defaultValue - Default value if property not found
|
|
36
|
+
* @returns Property value or default
|
|
37
|
+
*/
|
|
38
|
+
export function getSafeProperty(
|
|
39
|
+
obj: unknown,
|
|
40
|
+
property: string,
|
|
41
|
+
defaultValue: unknown = undefined
|
|
42
|
+
): unknown {
|
|
43
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
44
|
+
return defaultValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Split property path by dots
|
|
48
|
+
const parts = property.split('.');
|
|
49
|
+
|
|
50
|
+
// Validate all parts are safe property names
|
|
51
|
+
if (!parts.every(isSafePropertyName)) {
|
|
52
|
+
return defaultValue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Navigate safely through the object
|
|
56
|
+
let current: unknown = obj;
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
if (typeof current !== 'object' || current === null) {
|
|
59
|
+
return defaultValue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const next = (current as Record<string, unknown>)[part];
|
|
63
|
+
if (next === undefined) {
|
|
64
|
+
return defaultValue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
current = next;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Safe filter operation on array of objects
|
|
75
|
+
* Prevents injection by validating property names
|
|
76
|
+
* @param items - Array to filter
|
|
77
|
+
* @param property - Property to filter by (no dot notation)
|
|
78
|
+
* @param value - Value to match
|
|
79
|
+
* @param exact - If true, use exact match; if false, substring match
|
|
80
|
+
* @returns Filtered array
|
|
81
|
+
*/
|
|
82
|
+
export function safeFilter<T extends Record<string, unknown>>(
|
|
83
|
+
items: T[],
|
|
84
|
+
property: string,
|
|
85
|
+
value: unknown,
|
|
86
|
+
exact: boolean = true
|
|
87
|
+
): T[] {
|
|
88
|
+
// Validate property name
|
|
89
|
+
if (!isSafePropertyName(property)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Array.isArray(items)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return items.filter((item) => {
|
|
98
|
+
const itemValue = item[property];
|
|
99
|
+
|
|
100
|
+
if (exact) {
|
|
101
|
+
// Exact match
|
|
102
|
+
return itemValue === value;
|
|
103
|
+
} else {
|
|
104
|
+
// Substring match (only for strings)
|
|
105
|
+
if (typeof itemValue === 'string' && typeof value === 'string') {
|
|
106
|
+
return itemValue.includes(value);
|
|
107
|
+
}
|
|
108
|
+
return itemValue === value;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Safe search operation with multiple conditions
|
|
115
|
+
* Uses AND logic for multiple conditions
|
|
116
|
+
* @param items - Array to search
|
|
117
|
+
* @param conditions - Object with property-value pairs to match
|
|
118
|
+
* @returns Filtered array
|
|
119
|
+
*/
|
|
120
|
+
export function safeSearch<T extends Record<string, unknown>>(
|
|
121
|
+
items: T[],
|
|
122
|
+
conditions: SafeQueryParams
|
|
123
|
+
): T[] {
|
|
124
|
+
if (!Array.isArray(items)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return items.filter((item) => {
|
|
129
|
+
// All conditions must match (AND logic)
|
|
130
|
+
return Object.entries(conditions).every(([property, value]) => {
|
|
131
|
+
// Validate property name
|
|
132
|
+
if (!isSafePropertyName(property)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return item[property] === value;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sanitize string for use in LIKE queries (if migrating to SQL)
|
|
143
|
+
* Escapes SQL wildcards and special characters
|
|
144
|
+
* @param str - String to sanitize
|
|
145
|
+
* @returns Sanitized string
|
|
146
|
+
*/
|
|
147
|
+
export function sanitizeForLikeQuery(str: string): string {
|
|
148
|
+
if (typeof str !== 'string') {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Escape SQL wildcard characters: %, _
|
|
153
|
+
// Also escape backslash if used as escape character
|
|
154
|
+
return str
|
|
155
|
+
.replace(/\\/g, '\\\\') // Backslash first
|
|
156
|
+
.replace(/%/g, '\\%')
|
|
157
|
+
.replace(/_/g, '\\_');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Prepare a value for safe database insertion
|
|
162
|
+
* Handles type coercion and sanitization
|
|
163
|
+
* @param value - Value to prepare
|
|
164
|
+
* @returns Prepared value
|
|
165
|
+
*/
|
|
166
|
+
export function prepareDatabaseValue(value: unknown): unknown {
|
|
167
|
+
if (value === null || value === undefined) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
// For JSON storage, strings are safe as-is
|
|
173
|
+
// If migrating to SQL, use parameterized queries
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof value === 'object') {
|
|
182
|
+
// For arrays and objects, store as-is (JSON handles serialization)
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// For other types, convert to null
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Validate a database query object for safe execution
|
|
192
|
+
* Checks for suspicious patterns that might indicate injection
|
|
193
|
+
* @param query - Query object to validate
|
|
194
|
+
* @returns { valid: boolean; errors: string[] }
|
|
195
|
+
*/
|
|
196
|
+
export function validateDatabaseQuery(
|
|
197
|
+
query: unknown
|
|
198
|
+
): { valid: boolean; errors: string[] } {
|
|
199
|
+
const errors: string[] = [];
|
|
200
|
+
|
|
201
|
+
if (typeof query !== 'object' || query === null) {
|
|
202
|
+
errors.push('Query must be an object');
|
|
203
|
+
return { valid: false, errors };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const queryObj = query as Record<string, unknown>;
|
|
207
|
+
|
|
208
|
+
// Check for suspicious property names
|
|
209
|
+
for (const property of Object.keys(queryObj)) {
|
|
210
|
+
if (!isSafePropertyName(property)) {
|
|
211
|
+
errors.push(`Unsafe property name: ${property}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check for SQL injection patterns in string values
|
|
215
|
+
const value = queryObj[property];
|
|
216
|
+
if (typeof value === 'string') {
|
|
217
|
+
if (value.includes(';') || value.includes('--') || value.includes('/*')) {
|
|
218
|
+
errors.push(`Suspicious content in property: ${property}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { valid: errors.length === 0, errors };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Security recommendations for different database systems
|
|
228
|
+
*/
|
|
229
|
+
export const databaseSecurityGuides = {
|
|
230
|
+
json: {
|
|
231
|
+
name: 'JSON File Storage',
|
|
232
|
+
description: 'Current implementation',
|
|
233
|
+
practices: [
|
|
234
|
+
'Use isSafePropertyName() to validate property names',
|
|
235
|
+
'Use safeFilter() and safeSearch() for queries',
|
|
236
|
+
'Use getSafeProperty() for nested property access',
|
|
237
|
+
'Never use string interpolation for property access'
|
|
238
|
+
]
|
|
239
|
+
},
|
|
240
|
+
sql: {
|
|
241
|
+
name: 'SQL Databases (PostgreSQL, MySQL, etc.)',
|
|
242
|
+
description: 'For future migration',
|
|
243
|
+
practices: [
|
|
244
|
+
'Always use parameterized queries / prepared statements',
|
|
245
|
+
'Never concatenate user input into SQL strings',
|
|
246
|
+
'Use ORM libraries with built-in protection',
|
|
247
|
+
'Validate column/table names with whitelist',
|
|
248
|
+
'Use input validation before queries',
|
|
249
|
+
'Implement principle of least privilege for DB user'
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Example parameterized query pattern (for SQL migration reference)
|
|
256
|
+
*/
|
|
257
|
+
export function getParameterizedQueryExample(): string {
|
|
258
|
+
return `
|
|
259
|
+
// Example: PostgreSQL with parameterized query
|
|
260
|
+
const query = 'SELECT * FROM users WHERE email = $1';
|
|
261
|
+
const values = [userEmail];
|
|
262
|
+
const result = await db.query(query, values);
|
|
263
|
+
|
|
264
|
+
// Example: Using ORM (Prisma)
|
|
265
|
+
const user = await prisma.user.findUnique({
|
|
266
|
+
where: { email: userEmail }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// NEVER do this:
|
|
270
|
+
// const query = \`SELECT * FROM users WHERE email = '\${userEmail}'\`; // VULNERABLE
|
|
271
|
+
`;
|
|
272
|
+
}
|