|
1 | 1 | import { writeAllSync } from 'https://deno.land/[email protected]/streams/mod.ts'; |
2 | | -import { resolve, normalize } from 'https://deno.land/[email protected]/path/mod.ts'; |
3 | 2 | import validate from './validate.ts'; |
4 | 3 |
|
5 | 4 | const DEFAULT_TEMPLATE_DIRECTORY = 'templates'; |
6 | 5 |
|
7 | | -/** |
8 | | - * Validates that a path component is safe and does not contain: |
9 | | - * - Path traversal sequences (../) |
10 | | - * - Null bytes |
11 | | - * - Shell metacharacters that could be used for injection |
12 | | - * - Absolute paths when not expected |
13 | | - */ |
14 | | -const validatePathComponent = (input: string | undefined, name: string, required: boolean): string | undefined => { |
15 | | - if (input === undefined || input === '') { |
16 | | - if (required) { |
17 | | - throw new Error(`${name} is required but was not provided`); |
18 | | - } |
19 | | - return undefined; |
20 | | - } |
21 | | - |
22 | | - // Check for null bytes (can be used to bypass security checks) |
23 | | - if (input.includes('\0')) { |
24 | | - throw new Error(`${name} contains invalid null bytes`); |
25 | | - } |
26 | | - |
27 | | - // Check for path traversal attempts |
28 | | - const normalized = normalize(input); |
29 | | - if (normalized.includes('..') || input.includes('..')) { |
30 | | - throw new Error(`${name} contains path traversal sequences (..)`); |
31 | | - } |
32 | | - |
33 | | - // Check for dangerous shell metacharacters |
34 | | - const dangerousChars = /[;&|`$(){}[\]<>!#*?~\n\r]/; |
35 | | - if (dangerousChars.test(input)) { |
36 | | - throw new Error(`${name} contains potentially dangerous characters`); |
37 | | - } |
38 | | - |
39 | | - // Check for excessively long paths (DoS prevention) |
40 | | - const MAX_PATH_LENGTH = 4096; |
41 | | - if (input.length > MAX_PATH_LENGTH) { |
42 | | - throw new Error(`${name} exceeds maximum allowed length of ${MAX_PATH_LENGTH} characters`); |
43 | | - } |
44 | | - |
45 | | - return input; |
46 | | -}; |
47 | | - |
48 | | -/** |
49 | | - * Validates that the resolved path is within the expected base directory |
50 | | - */ |
51 | | -const validatePathWithinBase = (basePath: string, targetPath: string): void => { |
52 | | - const resolvedBase = resolve(basePath); |
53 | | - const resolvedTarget = resolve(targetPath); |
54 | | - |
55 | | - if (!resolvedTarget.startsWith(resolvedBase)) { |
56 | | - throw new Error(`Target path escapes the project root directory`); |
57 | | - } |
58 | | -}; |
59 | | - |
60 | | -/** |
61 | | - * Validates that the path exists and is a directory |
62 | | - */ |
63 | | -const validateDirectoryExists = (path: string): void => { |
64 | | - try { |
65 | | - const stat = Deno.statSync(path); |
66 | | - if (!stat.isDirectory) { |
67 | | - throw new Error(`Path exists but is not a directory: ${path}`); |
68 | | - } |
69 | | - } catch (error) { |
70 | | - if (error instanceof Deno.errors.NotFound) { |
71 | | - throw new Error(`Directory does not exist: ${path}`); |
72 | | - } |
73 | | - throw error; |
74 | | - } |
75 | | -}; |
76 | | - |
77 | | -const outputError = (message: string): void => { |
78 | | - const result = { status: 'error', detail: message }; |
| 6 | +const main = () => { |
| 7 | + const PROJECT_ROOT = Deno.args[0]; |
| 8 | + const TEMPLATE_DIRECTORY = Deno.args[1]; |
| 9 | + const TEMPLATES_PATH = `${PROJECT_ROOT}/${ |
| 10 | + TEMPLATE_DIRECTORY ?? DEFAULT_TEMPLATE_DIRECTORY |
| 11 | + }`; |
| 12 | + const result = validate(TEMPLATES_PATH); |
79 | 13 | writeAllSync( |
80 | 14 | Deno.stdout, |
81 | 15 | new TextEncoder().encode(JSON.stringify(result)), |
82 | 16 | ); |
83 | 17 | }; |
84 | 18 |
|
85 | | -const main = () => { |
86 | | - try { |
87 | | - // Validate PROJECT_ROOT |
88 | | - const PROJECT_ROOT = validatePathComponent(Deno.args[0], 'Project root path', true); |
89 | | - if (!PROJECT_ROOT) { |
90 | | - throw new Error('Project root path is required'); |
91 | | - } |
92 | | - |
93 | | - // Validate TEMPLATE_DIRECTORY (optional) |
94 | | - const TEMPLATE_DIRECTORY = validatePathComponent(Deno.args[1], 'Template directory', false) |
95 | | - ?? DEFAULT_TEMPLATE_DIRECTORY; |
96 | | - |
97 | | - // Validate the template directory name itself |
98 | | - validatePathComponent(TEMPLATE_DIRECTORY, 'Template directory', false); |
99 | | - |
100 | | - // Construct and validate the full templates path |
101 | | - const TEMPLATES_PATH = `${PROJECT_ROOT}/${TEMPLATE_DIRECTORY}`; |
102 | | - |
103 | | - // Ensure the templates path stays within the project root |
104 | | - validatePathWithinBase(PROJECT_ROOT, TEMPLATES_PATH); |
105 | | - |
106 | | - // Verify the directory exists |
107 | | - validateDirectoryExists(TEMPLATES_PATH); |
108 | | - |
109 | | - const result = validate(TEMPLATES_PATH); |
110 | | - writeAllSync( |
111 | | - Deno.stdout, |
112 | | - new TextEncoder().encode(JSON.stringify(result)), |
113 | | - ); |
114 | | - } catch (error) { |
115 | | - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; |
116 | | - outputError(errorMessage); |
117 | | - Deno.exit(1); |
118 | | - } |
119 | | -}; |
120 | | - |
121 | 19 | main(); |
0 commit comments