Skip to content

Commit 316f407

Browse files
committed
fix: directly copy path validator code over
1 parent eb9b744 commit 316f407

File tree

4 files changed

+107
-21
lines changed

4 files changed

+107
-21
lines changed

package-lock.json

Lines changed: 3 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"@octokit/rest": "^20.1.1",
4242
"@opengovsg/formsg-sdk": "^0.11.0",
4343
"@opengovsg/sgid-client": "^2.0.0",
44-
"@opengovsg/starter-kitty-validators": "^1.2.11",
4544
"@slack/bolt": "^3.19.0",
4645
"auto-bind": "^4.0.0",
4746
"aws-lambda": "^1.0.7",
@@ -113,7 +112,9 @@
113112
"validator": "^13.12.0",
114113
"winston": "^3.13.0",
115114
"winston-cloudwatch": "^6.3.0",
116-
"yaml": "^2.4.2"
115+
"yaml": "^2.4.2",
116+
"zod": "^3.25.76",
117+
"zod-validation-error": "^3.5.3"
117118
},
118119
"devDependencies": {
119120
"@octokit/types": "^6.35.0",

src/services/db/GitFileSystemService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import fs from "fs"
22
import path from "path"
33

4-
import { createPathSchema } from "@opengovsg/starter-kitty-validators"
54
import { err, errAsync, ok, okAsync, Result, ResultAsync } from "neverthrow"
65
import {
76
CleanOptions,
@@ -21,6 +20,8 @@ import { NotFoundError } from "@errors/NotFoundError"
2120

2221
import tracer from "@utils/tracer"
2322

23+
import { createPathSchema } from "@validators/path"
24+
2425
import {
2526
EFS_VOL_PATH_STAGING,
2627
EFS_VOL_PATH_STAGING_LITE,

src/validators/path.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import path from "path"
2+
3+
import { ZodSchema, z } from "zod"
4+
import { fromError } from "zod-validation-error"
5+
6+
class OptionsError extends Error {
7+
constructor(message: string) {
8+
super(message)
9+
this.name = "OptionsError"
10+
}
11+
}
12+
13+
/**
14+
* The options to use for path validation.
15+
*
16+
* @public
17+
*/
18+
interface PathValidatorOptions {
19+
/**
20+
* The base path to use for validation. This must be an absolute path.
21+
*
22+
* All provided paths, resolved relative to the working directory of the Node process,
23+
* must be within this directory (or its subdirectories), or they will be considered unsafe.
24+
* You should provide a safe base path that does not contain sensitive files or directories.
25+
*
26+
* @example `'/var/www'`
27+
*/
28+
basePath: string
29+
}
30+
31+
const optionsSchema = z.object({
32+
basePath: z
33+
.string()
34+
.refine(
35+
(basePath) =>
36+
basePath === path.resolve(basePath) && path.isAbsolute(basePath),
37+
"The base path must be an absolute path"
38+
),
39+
})
40+
41+
type ParsedPathValidatorOptions = z.infer<typeof optionsSchema>
42+
43+
export const isSafePath = (absPath: string, basePath: string): boolean => {
44+
// check for poison null bytes
45+
if (absPath.indexOf("\0") !== -1) {
46+
return false
47+
}
48+
// check for backslashes
49+
if (absPath.indexOf("\\") !== -1) {
50+
return false
51+
}
52+
53+
// check for dot segments, even if they don't normalize to anything
54+
if (absPath.includes("..")) {
55+
return false
56+
}
57+
58+
// check if the normalized path is within the provided 'safe' base path
59+
if (path.resolve(basePath, path.relative(basePath, absPath)) !== absPath) {
60+
return false
61+
}
62+
if (absPath.indexOf(basePath) !== 0) {
63+
return false
64+
}
65+
return true
66+
}
67+
68+
const createValidationSchema = (options: ParsedPathValidatorOptions) =>
69+
z
70+
.string()
71+
// resolve the path relative to the Node process's current working directory
72+
// since that's what fs operations will be relative to
73+
.transform((untrustedPath) => path.resolve(untrustedPath))
74+
// resolvedPath is now an absolute path
75+
.refine((resolvedPath) => isSafePath(resolvedPath, options.basePath), {
76+
message: "The provided path is unsafe.",
77+
})
78+
79+
const toSchema = (options: ParsedPathValidatorOptions) =>
80+
z.string().trim().pipe(createValidationSchema(options))
81+
82+
/**
83+
* Create a schema that validates user-supplied pathnames for filesystem operations.
84+
*
85+
* @param options - The options to use for validation
86+
* @throws {@link OptionsError} If the options are invalid
87+
* @returns A Zod schema that validates paths.
88+
*
89+
* @public
90+
*/
91+
export const createPathSchema = (
92+
options: PathValidatorOptions
93+
): ZodSchema<string> => {
94+
const result = optionsSchema.safeParse(options)
95+
if (result.success) {
96+
return toSchema(result.data)
97+
}
98+
throw new OptionsError(fromError(result.error).toString())
99+
}

0 commit comments

Comments
 (0)