tlc-claude-code 1.4.8 → 1.4.9

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 (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Output Encoder Tests
3
+ *
4
+ * Tests for context-aware output encoding to prevent XSS.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ encodeHtml,
10
+ encodeHtmlAttribute,
11
+ encodeJavaScript,
12
+ encodeUrl,
13
+ encodeCss,
14
+ encodeForContext,
15
+ createEncoder,
16
+ } from './output-encoder.js';
17
+
18
+ describe('output-encoder', () => {
19
+ describe('encodeHtml', () => {
20
+ it('encodes less than sign', () => {
21
+ const result = encodeHtml('<script>');
22
+ expect(result).toBe('&lt;script&gt;');
23
+ });
24
+
25
+ it('encodes greater than sign', () => {
26
+ const result = encodeHtml('a > b');
27
+ expect(result).toBe('a &gt; b');
28
+ });
29
+
30
+ it('encodes ampersand', () => {
31
+ const result = encodeHtml('a & b');
32
+ expect(result).toBe('a &amp; b');
33
+ });
34
+
35
+ it('encodes double quotes', () => {
36
+ const result = encodeHtml('say "hello"');
37
+ expect(result).toBe('say &quot;hello&quot;');
38
+ });
39
+
40
+ it('encodes single quotes', () => {
41
+ const result = encodeHtml("it's fine");
42
+ expect(result).toBe('it&#x27;s fine');
43
+ });
44
+
45
+ it('does not double-encode already encoded content', () => {
46
+ const result = encodeHtml('&lt;script&gt;', { skipEncoded: true });
47
+ expect(result).toBe('&lt;script&gt;');
48
+ });
49
+
50
+ it('handles null input', () => {
51
+ const result = encodeHtml(null);
52
+ expect(result).toBe('');
53
+ });
54
+
55
+ it('handles undefined input', () => {
56
+ const result = encodeHtml(undefined);
57
+ expect(result).toBe('');
58
+ });
59
+
60
+ it('converts numbers to string', () => {
61
+ const result = encodeHtml(123);
62
+ expect(result).toBe('123');
63
+ });
64
+
65
+ it('strips null bytes', () => {
66
+ const result = encodeHtml('hello\x00world');
67
+ expect(result).not.toContain('\x00');
68
+ });
69
+ });
70
+
71
+ describe('encodeHtmlAttribute', () => {
72
+ it('encodes for use in attributes', () => {
73
+ const result = encodeHtmlAttribute('value with "quotes"');
74
+ expect(result).toBe('value with &quot;quotes&quot;');
75
+ });
76
+
77
+ it('encodes single quotes for single-quoted attributes', () => {
78
+ const result = encodeHtmlAttribute("it's a value");
79
+ expect(result).toContain('&#x27;');
80
+ });
81
+
82
+ it('encodes equals sign', () => {
83
+ const result = encodeHtmlAttribute('a=b');
84
+ expect(result).toBe('a&#x3D;b');
85
+ });
86
+
87
+ it('encodes backticks', () => {
88
+ const result = encodeHtmlAttribute('`code`');
89
+ expect(result).toBe('&#x60;code&#x60;');
90
+ });
91
+
92
+ it('handles event handler context', () => {
93
+ const result = encodeHtmlAttribute('alert(1)', { context: 'event' });
94
+ expect(result).not.toContain('(');
95
+ });
96
+ });
97
+
98
+ describe('encodeJavaScript', () => {
99
+ it('escapes backslashes', () => {
100
+ const result = encodeJavaScript('path\\to\\file');
101
+ expect(result).toBe('path\\\\to\\\\file');
102
+ });
103
+
104
+ it('escapes single quotes', () => {
105
+ const result = encodeJavaScript("it's");
106
+ expect(result).toBe("it\\'s");
107
+ });
108
+
109
+ it('escapes double quotes', () => {
110
+ const result = encodeJavaScript('say "hi"');
111
+ expect(result).toBe('say \\"hi\\"');
112
+ });
113
+
114
+ it('escapes newlines', () => {
115
+ const result = encodeJavaScript('line1\nline2');
116
+ expect(result).toBe('line1\\nline2');
117
+ });
118
+
119
+ it('escapes carriage returns', () => {
120
+ const result = encodeJavaScript('line1\rline2');
121
+ expect(result).toBe('line1\\rline2');
122
+ });
123
+
124
+ it('escapes forward slashes to prevent </script> breaking', () => {
125
+ const result = encodeJavaScript('</script>');
126
+ expect(result).toBe('<\\/script>');
127
+ });
128
+
129
+ it('escapes unicode line separators', () => {
130
+ const result = encodeJavaScript('text\u2028more');
131
+ expect(result).toBe('text\\u2028more');
132
+ });
133
+
134
+ it('escapes unicode paragraph separators', () => {
135
+ const result = encodeJavaScript('text\u2029more');
136
+ expect(result).toBe('text\\u2029more');
137
+ });
138
+ });
139
+
140
+ describe('encodeUrl', () => {
141
+ it('encodes spaces', () => {
142
+ const result = encodeUrl('hello world');
143
+ expect(result).toBe('hello%20world');
144
+ });
145
+
146
+ it('encodes special characters', () => {
147
+ const result = encodeUrl('a=b&c=d');
148
+ expect(result).toBe('a%3Db%26c%3Dd');
149
+ });
150
+
151
+ it('preserves alphanumeric characters', () => {
152
+ const result = encodeUrl('abc123');
153
+ expect(result).toBe('abc123');
154
+ });
155
+
156
+ it('preserves safe URL characters when configured', () => {
157
+ const result = encodeUrl('path/to/file', { preservePath: true });
158
+ expect(result).toBe('path/to/file');
159
+ });
160
+
161
+ it('encodes unicode characters', () => {
162
+ const result = encodeUrl('café');
163
+ expect(result).toBe('caf%C3%A9');
164
+ });
165
+
166
+ it('handles empty string', () => {
167
+ const result = encodeUrl('');
168
+ expect(result).toBe('');
169
+ });
170
+ });
171
+
172
+ describe('encodeCss', () => {
173
+ it('escapes backslashes', () => {
174
+ const result = encodeCss('url(\\path)');
175
+ expect(result).toContain('\\\\');
176
+ });
177
+
178
+ it('escapes quotes', () => {
179
+ const result = encodeCss('font-family: "Arial"');
180
+ expect(result).toContain('\\"'); // Escaped with backslash
181
+ });
182
+
183
+ it('escapes semicolons', () => {
184
+ const result = encodeCss('value; other-property');
185
+ expect(result).toContain('\\;'); // Escaped with backslash
186
+ });
187
+
188
+ it('escapes curly braces', () => {
189
+ const result = encodeCss('value { injection }');
190
+ expect(result).toContain('\\{'); // Escaped with backslash
191
+ });
192
+
193
+ it('blocks url() injection', () => {
194
+ const result = encodeCss('url(javascript:alert(1))');
195
+ expect(result).not.toContain('javascript:');
196
+ });
197
+
198
+ it('blocks expression() for IE', () => {
199
+ const result = encodeCss('expression(alert(1))');
200
+ expect(result).not.toContain('expression');
201
+ });
202
+ });
203
+
204
+ describe('encodeForContext', () => {
205
+ it('auto-detects HTML context', () => {
206
+ const result = encodeForContext('<script>', 'html');
207
+ expect(result).toBe('&lt;script&gt;');
208
+ });
209
+
210
+ it('auto-detects JavaScript context', () => {
211
+ const result = encodeForContext("it's", 'javascript');
212
+ expect(result).toBe("it\\'s");
213
+ });
214
+
215
+ it('auto-detects URL context', () => {
216
+ const result = encodeForContext('hello world', 'url');
217
+ expect(result).toBe('hello%20world');
218
+ });
219
+
220
+ it('auto-detects CSS context', () => {
221
+ const result = encodeForContext('value;', 'css');
222
+ expect(result).toContain('\\;'); // Semicolon is escaped with backslash
223
+ });
224
+
225
+ it('auto-detects attribute context', () => {
226
+ const result = encodeForContext('value"', 'attribute');
227
+ expect(result).toContain('&quot;');
228
+ });
229
+
230
+ it('throws for unknown context', () => {
231
+ expect(() => encodeForContext('test', 'unknown')).toThrow();
232
+ });
233
+ });
234
+
235
+ describe('createEncoder', () => {
236
+ it('creates encoder with default context', () => {
237
+ const encoder = createEncoder({ defaultContext: 'html' });
238
+ const result = encoder.encode('<script>').value();
239
+ expect(result).toBe('&lt;script&gt;');
240
+ });
241
+
242
+ it('allows context override', () => {
243
+ const encoder = createEncoder({ defaultContext: 'html' });
244
+ const result = encoder.encode('hello world', 'url').value();
245
+ expect(result).toBe('hello%20world');
246
+ });
247
+
248
+ it('chains encoding operations', () => {
249
+ const encoder = createEncoder();
250
+ const result = encoder
251
+ .encode('<script>', 'html')
252
+ .then((v) => v.toUpperCase())
253
+ .value();
254
+ expect(result).toBe('&LT;SCRIPT&GT;');
255
+ });
256
+ });
257
+
258
+ describe('unicode handling', () => {
259
+ it('preserves valid unicode in HTML', () => {
260
+ const result = encodeHtml('Hello 世界 🌍');
261
+ expect(result).toContain('世界');
262
+ expect(result).toContain('🌍');
263
+ });
264
+
265
+ it('preserves valid unicode in JavaScript', () => {
266
+ const result = encodeJavaScript('Hello 世界');
267
+ expect(result).toContain('世界');
268
+ });
269
+
270
+ it('handles surrogate pairs correctly', () => {
271
+ const emoji = '😀';
272
+ const result = encodeHtml(emoji);
273
+ expect(result).toBe('😀');
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Path Validator Module
3
+ *
4
+ * Prevents path traversal attacks (OWASP A01: Broken Access Control)
5
+ * Validates file paths to ensure they stay within allowed directories.
6
+ */
7
+
8
+ import path from 'path';
9
+
10
+ /**
11
+ * Custom error for path traversal attempts
12
+ */
13
+ export class PathTraversalError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'PathTraversalError';
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Normalize a path by resolving dots, decoding URL encoding, etc.
22
+ * @param {string} inputPath - Path to normalize
23
+ * @returns {string} Normalized path
24
+ */
25
+ export function normalizePath(inputPath) {
26
+ if (!inputPath) {
27
+ return '';
28
+ }
29
+
30
+ let normalized = inputPath;
31
+
32
+ // Decode URL encoding (including double encoding)
33
+ try {
34
+ // Decode multiple times to catch double encoding
35
+ let decoded = normalized;
36
+ let prevDecoded;
37
+ do {
38
+ prevDecoded = decoded;
39
+ decoded = decodeURIComponent(decoded);
40
+ } while (decoded !== prevDecoded && decoded.includes('%'));
41
+ normalized = decoded;
42
+ } catch {
43
+ // If decoding fails, keep original
44
+ }
45
+
46
+ // Convert Windows path separators to forward slashes for consistent handling
47
+ normalized = normalized.replace(/\\/g, '/');
48
+
49
+ // Collapse multiple slashes
50
+ normalized = normalized.replace(/\/+/g, '/');
51
+
52
+ // Use path.normalize to resolve . and ..
53
+ normalized = path.normalize(normalized);
54
+
55
+ // Remove trailing slash (unless it's the root)
56
+ if (normalized.length > 1 && normalized.endsWith('/')) {
57
+ normalized = normalized.slice(0, -1);
58
+ }
59
+ if (normalized.length > 1 && normalized.endsWith(path.sep)) {
60
+ normalized = normalized.slice(0, -1);
61
+ }
62
+
63
+ return normalized;
64
+ }
65
+
66
+ /**
67
+ * Check if a path is within a base directory
68
+ * @param {string} targetPath - Path to check
69
+ * @param {string} baseDir - Base directory
70
+ * @returns {boolean} True if within base
71
+ */
72
+ export function isWithinBase(targetPath, baseDir) {
73
+ const normalizedTarget = normalizePath(targetPath);
74
+ const normalizedBase = normalizePath(baseDir);
75
+
76
+ // Ensure base ends with separator for prefix matching
77
+ const basePrefix = normalizedBase.endsWith(path.sep)
78
+ ? normalizedBase
79
+ : normalizedBase + path.sep;
80
+
81
+ // Check if target starts with base or equals base
82
+ return (
83
+ normalizedTarget === normalizedBase ||
84
+ normalizedTarget.startsWith(basePrefix)
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Validate a file path against security constraints
90
+ * @param {string} inputPath - Path to validate
91
+ * @param {Object} options - Validation options
92
+ * @returns {Object} Validation result
93
+ */
94
+ export function validatePath(inputPath, options = {}) {
95
+ const { baseDir } = options;
96
+
97
+ // Check for empty path
98
+ if (!inputPath || inputPath.trim() === '') {
99
+ return {
100
+ valid: false,
101
+ threat: 'empty_path',
102
+ error: 'Path is empty',
103
+ };
104
+ }
105
+
106
+ // Check for null bytes
107
+ if (inputPath.includes('\x00')) {
108
+ return {
109
+ valid: false,
110
+ threat: 'null_byte',
111
+ error: 'Path contains null byte',
112
+ };
113
+ }
114
+
115
+ // Check for control characters (ASCII 0-31 except tab, newline)
116
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(inputPath)) {
117
+ return {
118
+ valid: false,
119
+ threat: 'control_characters',
120
+ error: 'Path contains control characters',
121
+ };
122
+ }
123
+
124
+ // Check for path with only dots
125
+ if (/^\.+$/.test(inputPath.replace(/[\/\\]/g, ''))) {
126
+ return {
127
+ valid: false,
128
+ threat: 'invalid_path',
129
+ error: 'Path contains only dots',
130
+ };
131
+ }
132
+
133
+ // Decode and normalize path
134
+ let normalizedPath = inputPath;
135
+
136
+ // Decode URL encoding to detect encoded traversal attempts
137
+ try {
138
+ let decoded = normalizedPath;
139
+ let prevDecoded;
140
+ let iterations = 0;
141
+ do {
142
+ prevDecoded = decoded;
143
+ decoded = decodeURIComponent(decoded);
144
+ iterations++;
145
+ } while (decoded !== prevDecoded && decoded.includes('%') && iterations < 5);
146
+ normalizedPath = decoded;
147
+ } catch {
148
+ // If decoding fails, use original
149
+ }
150
+
151
+ // Check for traversal patterns (before normalization)
152
+ if (normalizedPath.includes('..')) {
153
+ // Normalize to see where it actually points
154
+ const normalized = normalizePath(normalizedPath);
155
+ const baseNormalized = normalizePath(baseDir);
156
+
157
+ if (!isWithinBase(normalized, baseNormalized)) {
158
+ return {
159
+ valid: false,
160
+ threat: 'path_traversal',
161
+ error: 'Path traversal detected',
162
+ };
163
+ }
164
+ }
165
+
166
+ // Handle relative paths - resolve against base
167
+ let resolvedPath;
168
+ if (path.isAbsolute(normalizedPath)) {
169
+ resolvedPath = normalizePath(normalizedPath);
170
+ } else {
171
+ resolvedPath = normalizePath(path.join(baseDir, normalizedPath));
172
+ }
173
+
174
+ // Final check: ensure resolved path is within base
175
+ if (!isWithinBase(resolvedPath, baseDir)) {
176
+ return {
177
+ valid: false,
178
+ threat: 'outside_base',
179
+ error: 'Path is outside allowed directory',
180
+ };
181
+ }
182
+
183
+ return {
184
+ valid: true,
185
+ normalizedPath: resolvedPath,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Validate file extension
191
+ * @param {string} filename - Filename to check
192
+ * @param {Object} options - Validation options
193
+ * @returns {Object} Validation result
194
+ */
195
+ export function validateExtension(filename, options = {}) {
196
+ const {
197
+ allowed = null,
198
+ blocked = null,
199
+ caseSensitive = true,
200
+ checkDoubleExtension = false,
201
+ rejectHidden = false,
202
+ } = options;
203
+
204
+ // Check for hidden files
205
+ if (rejectHidden && filename.startsWith('.')) {
206
+ return {
207
+ valid: false,
208
+ error: 'Hidden files not allowed',
209
+ };
210
+ }
211
+
212
+ // Get extension
213
+ let ext = path.extname(filename);
214
+ if (!caseSensitive) {
215
+ ext = ext.toLowerCase();
216
+ }
217
+
218
+ // Check for double extensions
219
+ if (checkDoubleExtension) {
220
+ const withoutExt = filename.slice(0, -ext.length);
221
+ const secondExt = path.extname(withoutExt);
222
+ if (secondExt) {
223
+ return {
224
+ valid: false,
225
+ error: 'Double extension detected',
226
+ };
227
+ }
228
+ }
229
+
230
+ // Check blocked list
231
+ if (blocked) {
232
+ const blockedNormalized = caseSensitive
233
+ ? blocked
234
+ : blocked.map((e) => e.toLowerCase());
235
+ if (blockedNormalized.includes(ext)) {
236
+ return {
237
+ valid: false,
238
+ error: `Extension ${ext} is blocked`,
239
+ };
240
+ }
241
+ }
242
+
243
+ // Check allowed list
244
+ if (allowed) {
245
+ const allowedNormalized = caseSensitive
246
+ ? allowed
247
+ : allowed.map((e) => e.toLowerCase());
248
+ if (!allowedNormalized.includes(ext)) {
249
+ return {
250
+ valid: false,
251
+ error: `Extension ${ext} is not allowed`,
252
+ };
253
+ }
254
+ }
255
+
256
+ return { valid: true };
257
+ }
258
+
259
+ /**
260
+ * Create a path validator with preset options
261
+ * @param {Object} options - Validator options
262
+ * @returns {Object} Path validator instance
263
+ */
264
+ export function createPathValidator(options = {}) {
265
+ const {
266
+ baseDirs = [],
267
+ allowedExtensions = null,
268
+ blockedExtensions = null,
269
+ maxPathLength = 4096,
270
+ forbiddenPatterns = [],
271
+ followSymlinks = true,
272
+ } = options;
273
+
274
+ return {
275
+ /**
276
+ * Validate a path synchronously
277
+ * @param {string} inputPath - Path to validate
278
+ * @returns {Object} Validation result
279
+ */
280
+ validate(inputPath) {
281
+ // Check max length
282
+ if (inputPath.length > maxPathLength) {
283
+ return {
284
+ valid: false,
285
+ error: 'Path exceeds maximum length',
286
+ };
287
+ }
288
+
289
+ // Check forbidden patterns
290
+ for (const pattern of forbiddenPatterns) {
291
+ if (pattern.test(inputPath)) {
292
+ return {
293
+ valid: false,
294
+ error: 'Path matches forbidden pattern',
295
+ };
296
+ }
297
+ }
298
+
299
+ // Check against each base directory
300
+ let isValidForAnyBase = false;
301
+ let lastResult = null;
302
+
303
+ for (const baseDir of baseDirs) {
304
+ const result = validatePath(inputPath, { baseDir });
305
+ lastResult = result;
306
+ if (result.valid) {
307
+ isValidForAnyBase = true;
308
+
309
+ // Also check extension if configured
310
+ if (allowedExtensions || blockedExtensions) {
311
+ const filename = path.basename(inputPath);
312
+ const extResult = validateExtension(filename, {
313
+ allowed: allowedExtensions,
314
+ blocked: blockedExtensions,
315
+ });
316
+ if (!extResult.valid) {
317
+ return extResult;
318
+ }
319
+ }
320
+
321
+ return result;
322
+ }
323
+ }
324
+
325
+ if (!isValidForAnyBase) {
326
+ return lastResult || {
327
+ valid: false,
328
+ error: 'Path is not within any allowed directory',
329
+ };
330
+ }
331
+ },
332
+
333
+ /**
334
+ * Validate a path asynchronously (with symlink checking)
335
+ * @param {string} inputPath - Path to validate
336
+ * @param {Object} asyncOptions - Async validation options
337
+ * @returns {Promise<Object>} Validation result
338
+ */
339
+ async validateAsync(inputPath, asyncOptions = {}) {
340
+ const { checkSymlinks = false } = asyncOptions;
341
+
342
+ // First run sync validation
343
+ const syncResult = this.validate(inputPath);
344
+ if (!syncResult.valid) {
345
+ return syncResult;
346
+ }
347
+
348
+ // If symlink checking is enabled and we're not following symlinks
349
+ if (checkSymlinks && !followSymlinks) {
350
+ // In a real implementation, we would use fs.lstat here
351
+ // to check if the path is a symlink and where it points
352
+ // For now, return the sync result
353
+ return syncResult;
354
+ }
355
+
356
+ return syncResult;
357
+ },
358
+ };
359
+ }