|
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,22 +366,32 @@ export default class GitFileSystemService { |
366 | 366 | const efsVolPath = isStaging |
367 | 367 | ? EFS_VOL_PATH_STAGING |
368 | 368 | : EFS_VOL_PATH_STAGING_LITE |
369 | | - return ResultAsync.fromPromise( |
370 | | - fs.promises.stat(`${efsVolPath}/${repoName}/${filePath}`), |
371 | | - (error) => { |
372 | | - if (error instanceof Error && error.message.includes("ENOENT")) { |
373 | | - return new NotFoundError("File/Directory does not exist") |
374 | | - } |
375 | 369 |
|
376 | | - logger.error(`Error when reading ${filePath}: ${error}`) |
| 370 | + // Validate that the filePath is a valid relative path to prevent directory |
| 371 | + // traversal attacks |
| 372 | + const repoBaseDirectory = `${efsVolPath}/${repoName}` |
| 373 | + const fullFilePath = path.resolve(repoBaseDirectory, filePath) |
| 374 | + const pathSchema = createPathSchema({ basePath: repoBaseDirectory }) |
| 375 | + const parsedPathResult = pathSchema.safeParse(fullFilePath) |
377 | 376 |
|
378 | | - if (error instanceof Error) { |
379 | | - return new GitFileSystemError("Unable to read file/directory") |
380 | | - } |
| 377 | + if (!parsedPathResult.success) { |
| 378 | + logger.error(`Invalid file path: ${filePath} for repo: ${repoName}`) |
| 379 | + return errAsync(new BadRequestError("Invalid file path")) |
| 380 | + } |
381 | 381 |
|
382 | | - return new GitFileSystemError("An unknown error occurred") |
| 382 | + return ResultAsync.fromPromise(fs.promises.stat(fullFilePath), (error) => { |
| 383 | + if (error instanceof Error && error.message.includes("ENOENT")) { |
| 384 | + return new NotFoundError("File/Directory does not exist") |
383 | 385 | } |
384 | | - ) |
| 386 | + |
| 387 | + logger.error(`Error when reading ${filePath}: ${error}`) |
| 388 | + |
| 389 | + if (error instanceof Error) { |
| 390 | + return new GitFileSystemError("Unable to read file/directory") |
| 391 | + } |
| 392 | + |
| 393 | + return new GitFileSystemError("An unknown error occurred") |
| 394 | + }) |
385 | 395 | } |
386 | 396 |
|
387 | 397 | /** |
@@ -985,35 +995,39 @@ export default class GitFileSystemService { |
985 | 995 | encoding: "utf-8" | "base64" = "utf-8" |
986 | 996 | ): ResultAsync<GitFile, GitFileSystemError | NotFoundError> { |
987 | 997 | 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 | | - } |
| 998 | + return this.getFilePathStats(repoName, filePath, true) |
| 999 | + .andThen(() => |
| 1000 | + ResultAsync.combine([ |
| 1001 | + ResultAsync.fromPromise( |
| 1002 | + fs.promises.readFile( |
| 1003 | + `${defaultEfsVolPath}/${repoName}/${filePath}`, |
| 1004 | + encoding |
| 1005 | + ), |
| 1006 | + (error) => { |
| 1007 | + if (error instanceof Error && error.message.includes("ENOENT")) { |
| 1008 | + return new NotFoundError("File does not exist") |
| 1009 | + } |
998 | 1010 |
|
999 | | - logger.error(`Error when reading ${filePath}: ${error}`) |
| 1011 | + logger.error(`Error when reading ${filePath}: ${error}`) |
1000 | 1012 |
|
1001 | | - if (error instanceof Error) { |
1002 | | - return new GitFileSystemError("Unable to read file") |
1003 | | - } |
| 1013 | + if (error instanceof Error) { |
| 1014 | + return new GitFileSystemError("Unable to read file") |
| 1015 | + } |
1004 | 1016 |
|
1005 | | - return new GitFileSystemError("An unknown error occurred") |
| 1017 | + return new GitFileSystemError("An unknown error occurred") |
| 1018 | + } |
| 1019 | + ), |
| 1020 | + this.getGitBlobHash(repoName, filePath, true), |
| 1021 | + ]) |
| 1022 | + ) |
| 1023 | + .map((contentAndHash) => { |
| 1024 | + const [content, sha] = contentAndHash |
| 1025 | + const result: GitFile = { |
| 1026 | + content, |
| 1027 | + sha, |
1006 | 1028 | } |
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 | | - }) |
| 1029 | + return result |
| 1030 | + }) |
1017 | 1031 | } |
1018 | 1032 |
|
1019 | 1033 | getFileExtension(fileName: string): Result<string, MediaTypeError> { |
|
0 commit comments