Skip to content

Commit b5c1804

Browse files
committed
fix: validate path traversal
1 parent e5b2562 commit b5c1804

File tree

4 files changed

+91
-26
lines changed

4 files changed

+91
-26
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
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",
4445
"@slack/bolt": "^3.19.0",
4546
"auto-bind": "^4.0.0",
4647
"aws-lambda": "^1.0.7",

src/services/db/GitFileSystemService.ts

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import fs from "fs"
22
import path from "path"
33

4+
import { createPathSchema } from "@opengovsg/starter-kitty-validators"
45
import { err, errAsync, ok, okAsync, Result, ResultAsync } from "neverthrow"
56
import {
67
CleanOptions,
78
GitError,
89
SimpleGit,
910
DefaultLogFields,
1011
LogResult,
11-
ListLogLine,
1212
} from "simple-git"
1313

1414
import logger from "@logger/logger"
@@ -366,6 +366,18 @@ export default class GitFileSystemService {
366366
const efsVolPath = isStaging
367367
? EFS_VOL_PATH_STAGING
368368
: EFS_VOL_PATH_STAGING_LITE
369+
370+
// Validate that the filePath is a valid relative path to prevent directory
371+
// traversal attacks
372+
const repoBaseDirectory = `${efsVolPath}/${repoName}`
373+
const pathSchema = createPathSchema({ basePath: repoBaseDirectory })
374+
const parsedPathResult = pathSchema.safeParse(filePath)
375+
376+
if (!parsedPathResult.success) {
377+
logger.error(`Invalid file path: ${filePath} for repo: ${repoName}`)
378+
return errAsync(new BadRequestError("Invalid file path"))
379+
}
380+
369381
return ResultAsync.fromPromise(
370382
fs.promises.stat(`${efsVolPath}/${repoName}/${filePath}`),
371383
(error) => {
@@ -985,35 +997,39 @@ export default class GitFileSystemService {
985997
encoding: "utf-8" | "base64" = "utf-8"
986998
): ResultAsync<GitFile, GitFileSystemError | NotFoundError> {
987999
const defaultEfsVolPath = EFS_VOL_PATH_STAGING
988-
return ResultAsync.combine([
989-
ResultAsync.fromPromise(
990-
fs.promises.readFile(
991-
`${defaultEfsVolPath}/${repoName}/${filePath}`,
992-
encoding
993-
),
994-
(error) => {
995-
if (error instanceof Error && error.message.includes("ENOENT")) {
996-
return new NotFoundError("File does not exist")
997-
}
1000+
return this.getFilePathStats(repoName, filePath, true)
1001+
.andThen(() =>
1002+
ResultAsync.combine([
1003+
ResultAsync.fromPromise(
1004+
fs.promises.readFile(
1005+
`${defaultEfsVolPath}/${repoName}/${filePath}`,
1006+
encoding
1007+
),
1008+
(error) => {
1009+
if (error instanceof Error && error.message.includes("ENOENT")) {
1010+
return new NotFoundError("File does not exist")
1011+
}
9981012

999-
logger.error(`Error when reading ${filePath}: ${error}`)
1013+
logger.error(`Error when reading ${filePath}: ${error}`)
10001014

1001-
if (error instanceof Error) {
1002-
return new GitFileSystemError("Unable to read file")
1003-
}
1015+
if (error instanceof Error) {
1016+
return new GitFileSystemError("Unable to read file")
1017+
}
10041018

1005-
return new GitFileSystemError("An unknown error occurred")
1019+
return new GitFileSystemError("An unknown error occurred")
1020+
}
1021+
),
1022+
this.getGitBlobHash(repoName, filePath, true),
1023+
])
1024+
)
1025+
.map((contentAndHash) => {
1026+
const [content, sha] = contentAndHash
1027+
const result: GitFile = {
1028+
content,
1029+
sha,
10061030
}
1007-
),
1008-
this.getGitBlobHash(repoName, filePath, true),
1009-
]).map((contentAndHash) => {
1010-
const [content, sha] = contentAndHash
1011-
const result: GitFile = {
1012-
content,
1013-
sha,
1014-
}
1015-
return result
1016-
})
1031+
return result
1032+
})
10171033
}
10181034

10191035
getFileExtension(fileName: string): Result<string, MediaTypeError> {

src/services/db/__tests__/GitFileSystemService.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,16 @@ describe("GitFileSystemService", () => {
720720
expect(result._unsafeUnwrap().isDirectory()).toBeTrue()
721721
})
722722

723+
it("should return a BadRequestError if the path goes above the repo root", async () => {
724+
const result = await GitFileSystemService.getFilePathStats(
725+
"fake-repo",
726+
"../some-file",
727+
DEFAULT_BRANCH === "staging"
728+
)
729+
730+
expect(result._unsafeUnwrapErr()).toBeInstanceOf(BadRequestError)
731+
})
732+
723733
it("should return a NotFoundError for a non-existent path", async () => {
724734
const result = await GitFileSystemService.getFilePathStats(
725735
"fake-repo",

0 commit comments

Comments
 (0)