|
1 | 1 | import fs from "fs" |
2 | 2 | import path from "path" |
3 | 3 |
|
| 4 | +import { createPathSchema } from "@opengovsg/starter-kitty-validators" |
4 | 5 | import { err, errAsync, ok, okAsync, Result, ResultAsync } from "neverthrow" |
5 | 6 | import { |
6 | 7 | CleanOptions, |
7 | 8 | GitError, |
8 | 9 | SimpleGit, |
9 | 10 | DefaultLogFields, |
10 | 11 | LogResult, |
11 | | - ListLogLine, |
12 | 12 | } from "simple-git" |
13 | 13 |
|
14 | 14 | import logger from "@logger/logger" |
@@ -366,6 +366,18 @@ export default class GitFileSystemService { |
366 | 366 | const efsVolPath = isStaging |
367 | 367 | ? EFS_VOL_PATH_STAGING |
368 | 368 | : 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 | + |
369 | 381 | return ResultAsync.fromPromise( |
370 | 382 | fs.promises.stat(`${efsVolPath}/${repoName}/${filePath}`), |
371 | 383 | (error) => { |
@@ -985,35 +997,39 @@ export default class GitFileSystemService { |
985 | 997 | encoding: "utf-8" | "base64" = "utf-8" |
986 | 998 | ): ResultAsync<GitFile, GitFileSystemError | NotFoundError> { |
987 | 999 | 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 | + } |
998 | 1012 |
|
999 | | - logger.error(`Error when reading ${filePath}: ${error}`) |
| 1013 | + logger.error(`Error when reading ${filePath}: ${error}`) |
1000 | 1014 |
|
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 | + } |
1004 | 1018 |
|
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, |
1006 | 1030 | } |
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 | + }) |
1017 | 1033 | } |
1018 | 1034 |
|
1019 | 1035 | getFileExtension(fileName: string): Result<string, MediaTypeError> { |
|
0 commit comments