Skip to content

Commit 1f84c3c

Browse files
author
kaixuanxu
committed
chore: refine the user inputs for RCE potentials
1 parent dda5d3f commit 1f84c3c

File tree

2 files changed

+116
-8
lines changed

2 files changed

+116
-8
lines changed

.github/actions/validate-template/action.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,11 @@ runs:
2828

2929
- name: Validate template
3030
id: validate-template
31-
run: echo "::set-output name=result::$(deno run --allow-read ${{ github.action_path }}/src/main.ts ${{ inputs.path }} ${{ inputs.directory }})"
31+
env:
32+
INPUT_PATH: ${{ inputs.path }}
33+
INPUT_DIRECTORY: ${{ inputs.directory }}
34+
ACTION_PATH: ${{ github.action_path }}
35+
run: |
36+
result=$(deno run --allow-read "${ACTION_PATH}/src/main.ts" "${INPUT_PATH}" "${INPUT_DIRECTORY}")
37+
echo "result=${result}" >> "$GITHUB_OUTPUT"
3238
shell: bash
Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,121 @@
11
import { writeAllSync } from 'https://deno.land/[email protected]/streams/mod.ts';
2+
import { resolve, normalize } from 'https://deno.land/[email protected]/path/mod.ts';
23
import validate from './validate.ts';
34

45
const DEFAULT_TEMPLATE_DIRECTORY = 'templates';
56

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);
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 };
1379
writeAllSync(
1480
Deno.stdout,
1581
new TextEncoder().encode(JSON.stringify(result)),
1682
);
1783
};
1884

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+
19121
main();

0 commit comments

Comments
 (0)