safe-path-utils.ts 3.1 KB

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