@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.
Files changed (178) hide show
  1. package/dist/adapter-context.d.ts +19 -0
  2. package/dist/adapter-context.d.ts.map +1 -0
  3. package/dist/adapter-context.js +68 -0
  4. package/dist/adapter-context.js.map +1 -0
  5. package/dist/adapters/__tests__/adapter-tests.d.ts +7 -0
  6. package/dist/adapters/__tests__/adapter-tests.d.ts.map +1 -0
  7. package/dist/adapters/__tests__/adapter-tests.js +206 -0
  8. package/dist/adapters/__tests__/adapter-tests.js.map +1 -0
  9. package/dist/adapters/adapter.d.ts +60 -0
  10. package/dist/adapters/adapter.d.ts.map +1 -0
  11. package/dist/adapters/adapter.js +2 -0
  12. package/dist/adapters/adapter.js.map +1 -0
  13. package/dist/adapters/filesystem-adapter.d.ts +26 -0
  14. package/dist/adapters/filesystem-adapter.d.ts.map +1 -0
  15. package/dist/adapters/filesystem-adapter.js +148 -0
  16. package/dist/adapters/filesystem-adapter.js.map +1 -0
  17. package/dist/adapters/index.d.ts +6 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +5 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/mongodb-adapter.d.ts +27 -0
  22. package/dist/adapters/mongodb-adapter.d.ts.map +1 -0
  23. package/dist/adapters/mongodb-adapter.js +213 -0
  24. package/dist/adapters/mongodb-adapter.js.map +1 -0
  25. package/dist/adapters/postgres-adapter.d.ts +30 -0
  26. package/dist/adapters/postgres-adapter.d.ts.map +1 -0
  27. package/dist/adapters/postgres-adapter.js +237 -0
  28. package/dist/adapters/postgres-adapter.js.map +1 -0
  29. package/dist/adapters/sqlite-adapter.d.ts +26 -0
  30. package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
  31. package/dist/adapters/sqlite-adapter.js +261 -0
  32. package/dist/adapters/sqlite-adapter.js.map +1 -0
  33. package/dist/auth.d.ts +48 -0
  34. package/dist/auth.d.ts.map +1 -0
  35. package/dist/auth.js +205 -0
  36. package/dist/auth.js.map +1 -0
  37. package/dist/client-jwt.d.ts +30 -0
  38. package/dist/client-jwt.d.ts.map +1 -0
  39. package/dist/client-jwt.js +57 -0
  40. package/dist/client-jwt.js.map +1 -0
  41. package/dist/client-store.d.ts +31 -0
  42. package/dist/client-store.d.ts.map +1 -0
  43. package/dist/client-store.js +122 -0
  44. package/dist/client-store.js.map +1 -0
  45. package/dist/cors.d.ts +48 -0
  46. package/dist/cors.d.ts.map +1 -0
  47. package/dist/cors.js +88 -0
  48. package/dist/cors.js.map +1 -0
  49. package/dist/csrf.d.ts +57 -0
  50. package/dist/csrf.d.ts.map +1 -0
  51. package/dist/csrf.js +95 -0
  52. package/dist/csrf.js.map +1 -0
  53. package/dist/db.d.ts +22 -0
  54. package/dist/db.d.ts.map +1 -0
  55. package/dist/db.js +43 -0
  56. package/dist/db.js.map +1 -0
  57. package/dist/index.d.ts +35 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +36 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/input-validation.d.ts +78 -0
  62. package/dist/input-validation.d.ts.map +1 -0
  63. package/dist/input-validation.js +238 -0
  64. package/dist/input-validation.js.map +1 -0
  65. package/dist/oauth-callback.d.ts +31 -0
  66. package/dist/oauth-callback.d.ts.map +1 -0
  67. package/dist/oauth-callback.js +254 -0
  68. package/dist/oauth-callback.js.map +1 -0
  69. package/dist/oauth-providers.d.ts +92 -0
  70. package/dist/oauth-providers.d.ts.map +1 -0
  71. package/dist/oauth-providers.js +213 -0
  72. package/dist/oauth-providers.js.map +1 -0
  73. package/dist/oauth-types.d.ts +77 -0
  74. package/dist/oauth-types.d.ts.map +1 -0
  75. package/dist/oauth-types.js +2 -0
  76. package/dist/oauth-types.js.map +1 -0
  77. package/dist/password.d.ts +31 -0
  78. package/dist/password.d.ts.map +1 -0
  79. package/dist/password.js +54 -0
  80. package/dist/password.js.map +1 -0
  81. package/dist/providers/github-oauth.d.ts +58 -0
  82. package/dist/providers/github-oauth.d.ts.map +1 -0
  83. package/dist/providers/github-oauth.js +230 -0
  84. package/dist/providers/github-oauth.js.map +1 -0
  85. package/dist/providers/google-oauth.d.ts +46 -0
  86. package/dist/providers/google-oauth.d.ts.map +1 -0
  87. package/dist/providers/google-oauth.js +177 -0
  88. package/dist/providers/google-oauth.js.map +1 -0
  89. package/dist/providers/oidc-oauth.d.ts +85 -0
  90. package/dist/providers/oidc-oauth.d.ts.map +1 -0
  91. package/dist/providers/oidc-oauth.js +301 -0
  92. package/dist/providers/oidc-oauth.js.map +1 -0
  93. package/dist/rate-limit.d.ts +36 -0
  94. package/dist/rate-limit.d.ts.map +1 -0
  95. package/dist/rate-limit.js +88 -0
  96. package/dist/rate-limit.js.map +1 -0
  97. package/dist/rate-limiting.d.ts +113 -0
  98. package/dist/rate-limiting.d.ts.map +1 -0
  99. package/dist/rate-limiting.js +221 -0
  100. package/dist/rate-limiting.js.map +1 -0
  101. package/dist/security-headers.d.ts +54 -0
  102. package/dist/security-headers.d.ts.map +1 -0
  103. package/dist/security-headers.js +123 -0
  104. package/dist/security-headers.js.map +1 -0
  105. package/dist/session.d.ts +13 -0
  106. package/dist/session.d.ts.map +1 -0
  107. package/dist/session.js +33 -0
  108. package/dist/session.js.map +1 -0
  109. package/dist/sql-injection-prevention.d.ts +94 -0
  110. package/dist/sql-injection-prevention.d.ts.map +1 -0
  111. package/dist/sql-injection-prevention.js +222 -0
  112. package/dist/sql-injection-prevention.js.map +1 -0
  113. package/dist/token.d.ts +22 -0
  114. package/dist/token.d.ts.map +1 -0
  115. package/dist/token.js +31 -0
  116. package/dist/token.js.map +1 -0
  117. package/dist/types.d.ts +81 -0
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/dist/user.d.ts +33 -0
  122. package/dist/user.d.ts.map +1 -0
  123. package/dist/user.js +144 -0
  124. package/dist/user.js.map +1 -0
  125. package/package.json +48 -0
  126. package/src/adapter-context.ts +72 -0
  127. package/src/adapters/__tests__/adapter-tests.ts +254 -0
  128. package/src/adapters/__tests__/filesystem-adapter.test.ts +48 -0
  129. package/src/adapters/__tests__/mongodb-adapter.test.ts +64 -0
  130. package/src/adapters/__tests__/postgres-adapter.test.ts +62 -0
  131. package/src/adapters/__tests__/sqlite-adapter.test.ts +103 -0
  132. package/src/adapters/__tests__/test-fs-adapter.json +4 -0
  133. package/src/adapters/adapter.ts +72 -0
  134. package/src/adapters/filesystem-adapter.ts +153 -0
  135. package/src/adapters/index.ts +5 -0
  136. package/src/adapters/mongodb-adapter.ts +208 -0
  137. package/src/adapters/postgres-adapter.ts +261 -0
  138. package/src/adapters/sqlite-adapter.ts +284 -0
  139. package/src/auth.ts +239 -0
  140. package/src/client-jwt.test.ts +137 -0
  141. package/src/client-jwt.ts +67 -0
  142. package/src/client-store.test.ts +149 -0
  143. package/src/client-store.ts +144 -0
  144. package/src/cors.test.ts +175 -0
  145. package/src/cors.ts +115 -0
  146. package/src/csrf.test.ts +226 -0
  147. package/src/csrf.ts +126 -0
  148. package/src/db.ts +57 -0
  149. package/src/index.ts +143 -0
  150. package/src/input-validation.test.ts +347 -0
  151. package/src/input-validation.ts +307 -0
  152. package/src/integration.test.ts +322 -0
  153. package/src/oauth-callback.test.ts +282 -0
  154. package/src/oauth-callback.ts +323 -0
  155. package/src/oauth-providers.ts +232 -0
  156. package/src/oauth-types.ts +82 -0
  157. package/src/password.test.ts +89 -0
  158. package/src/password.ts +62 -0
  159. package/src/providers/github-oauth.test.ts +290 -0
  160. package/src/providers/github-oauth.ts +226 -0
  161. package/src/providers/google-oauth.test.ts +240 -0
  162. package/src/providers/google-oauth.ts +166 -0
  163. package/src/providers/oidc-oauth.test.ts +367 -0
  164. package/src/providers/oidc-oauth.ts +302 -0
  165. package/src/rate-limit.test.ts +308 -0
  166. package/src/rate-limit.ts +118 -0
  167. package/src/rate-limiting.test.ts +390 -0
  168. package/src/rate-limiting.ts +275 -0
  169. package/src/security-headers.test.ts +242 -0
  170. package/src/security-headers.ts +160 -0
  171. package/src/security-penetration.test.ts +705 -0
  172. package/src/session.ts +42 -0
  173. package/src/sql-injection-prevention.test.ts +337 -0
  174. package/src/sql-injection-prevention.ts +272 -0
  175. package/src/token.test.ts +67 -0
  176. package/src/token.ts +34 -0
  177. package/src/types.ts +87 -0
  178. 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
+ }