safe-path-utils.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import path from 'pathe';
  2. export const SUPPORTED_LOCALES = ['en_US', 'ja_JP', 'zh_CN'];
  3. /**
  4. * Validates that the given file path is within the base directory.
  5. * This prevents path traversal attacks where an attacker could use sequences
  6. * like '../' to access files outside the intended directory.
  7. *
  8. * @param filePath - The file path to validate
  9. * @param baseDir - The base directory that the file path should be within
  10. * @returns true if the path is valid, false otherwise
  11. */
  12. export function isPathWithinBase(filePath: string, baseDir: string): boolean {
  13. const resolvedBaseDir = path.resolve(baseDir);
  14. const resolvedFilePath = path.resolve(filePath);
  15. // Check if the resolved path starts with the base directory
  16. // We add path.sep to ensure we're checking a directory boundary
  17. // (e.g., /tmp/foo should not match /tmp/foobar)
  18. return (
  19. resolvedFilePath.startsWith(resolvedBaseDir + path.sep) ||
  20. resolvedFilePath === resolvedBaseDir
  21. );
  22. }
  23. /**
  24. * Validates that joining baseDir with fileName results in a path within baseDir.
  25. * This is useful for validating user-provided file names before using them.
  26. *
  27. * @param fileName - The file name to validate
  28. * @param baseDir - The base directory
  29. * @returns true if the resulting path is valid, false otherwise
  30. * @throws Error if path traversal is detected
  31. */
  32. export function assertFileNameSafeForBaseDir(
  33. fileName: string,
  34. baseDir: string,
  35. ): void {
  36. const resolvedBaseDir = path.resolve(baseDir);
  37. const resolvedFilePath = path.resolve(baseDir, fileName);
  38. const isValid =
  39. resolvedFilePath.startsWith(resolvedBaseDir + path.sep) ||
  40. resolvedFilePath === resolvedBaseDir;
  41. if (!isValid) {
  42. throw new Error('Invalid file path: path traversal detected');
  43. }
  44. }
  45. /**
  46. * Resolves a locale-specific template path safely, preventing path traversal attacks.
  47. * Falls back to 'en_US' if the locale is not in the supported list.
  48. *
  49. * @param locale - The locale string (e.g. 'en_US')
  50. * @param baseDir - The base directory for locale files
  51. * @param templateSubPath - The sub-path within the locale directory (e.g. 'notifications/event.ejs')
  52. * @returns The template path
  53. * @throws Error if path traversal is detected
  54. */
  55. export function resolveLocalePath(
  56. locale: string,
  57. baseDir: string,
  58. templateSubPath: string,
  59. ): string {
  60. const safeLocale = SUPPORTED_LOCALES.includes(locale) ? locale : 'en_US';
  61. return path.join(baseDir, safeLocale, templateSubPath);
  62. }
  63. /**
  64. * Validates that joining baseDir with fileName results in a path within baseDir.
  65. * This is useful for validating user-provided file names before using them.
  66. *
  67. * @param fileName - The file name to validate
  68. * @param baseDir - The base directory
  69. * @returns true if the resulting path is valid, false otherwise
  70. */
  71. export function isFileNameSafeForBaseDir(
  72. fileName: string,
  73. baseDir: string,
  74. ): boolean {
  75. const resolvedBaseDir = path.resolve(baseDir);
  76. const resolvedFilePath = path.resolve(baseDir, fileName);
  77. return (
  78. resolvedFilePath.startsWith(resolvedBaseDir + path.sep) ||
  79. resolvedFilePath === resolvedBaseDir
  80. );
  81. }