safe-path-utils.spec.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import path from 'pathe';
  2. import {
  3. assertFileNameSafeForBaseDir,
  4. isFileNameSafeForBaseDir,
  5. isPathWithinBase,
  6. } from './safe-path-utils';
  7. describe('path-utils', () => {
  8. describe('isPathWithinBase', () => {
  9. const baseDir = '/tmp/growi-export';
  10. describe('valid paths', () => {
  11. test('should return true for file directly in baseDir', () => {
  12. const filePath = '/tmp/growi-export/test.json';
  13. expect(isPathWithinBase(filePath, baseDir)).toBe(true);
  14. });
  15. test('should return true for file in subdirectory', () => {
  16. const filePath = '/tmp/growi-export/subdir/test.json';
  17. expect(isPathWithinBase(filePath, baseDir)).toBe(true);
  18. });
  19. test('should return true for baseDir itself', () => {
  20. expect(isPathWithinBase(baseDir, baseDir)).toBe(true);
  21. });
  22. test('should handle relative paths correctly', () => {
  23. const filePath = path.join(baseDir, 'test.json');
  24. expect(isPathWithinBase(filePath, baseDir)).toBe(true);
  25. });
  26. });
  27. describe('invalid paths (path traversal attacks)', () => {
  28. test('should return false for path outside baseDir', () => {
  29. const filePath = '/etc/passwd';
  30. expect(isPathWithinBase(filePath, baseDir)).toBe(false);
  31. });
  32. test('should return false for path traversal with ../', () => {
  33. const filePath = '/tmp/growi-export/../etc/passwd';
  34. expect(isPathWithinBase(filePath, baseDir)).toBe(false);
  35. });
  36. test('should return false for sibling directory', () => {
  37. const filePath = '/tmp/other-dir/test.json';
  38. expect(isPathWithinBase(filePath, baseDir)).toBe(false);
  39. });
  40. test('should return false for directory with similar prefix', () => {
  41. // /tmp/growi-export-evil should not match /tmp/growi-export
  42. const filePath = '/tmp/growi-export-evil/test.json';
  43. expect(isPathWithinBase(filePath, baseDir)).toBe(false);
  44. });
  45. });
  46. });
  47. describe('isFileNameSafeForBaseDir', () => {
  48. const baseDir = '/tmp/growi-export';
  49. describe('valid file names', () => {
  50. test('should return true for simple filename', () => {
  51. expect(isFileNameSafeForBaseDir('test.json', baseDir)).toBe(true);
  52. });
  53. test('should return true for filename in subdirectory', () => {
  54. expect(isFileNameSafeForBaseDir('subdir/test.json', baseDir)).toBe(
  55. true,
  56. );
  57. });
  58. test('should return true for deeply nested file', () => {
  59. expect(isFileNameSafeForBaseDir('a/b/c/d/test.json', baseDir)).toBe(
  60. true,
  61. );
  62. });
  63. });
  64. describe('path traversal attacks', () => {
  65. test('should return false for ../etc/passwd', () => {
  66. expect(isFileNameSafeForBaseDir('../etc/passwd', baseDir)).toBe(false);
  67. });
  68. test('should return false for ../../etc/passwd', () => {
  69. expect(isFileNameSafeForBaseDir('../../etc/passwd', baseDir)).toBe(
  70. false,
  71. );
  72. });
  73. test('should return false for subdir/../../../etc/passwd', () => {
  74. expect(
  75. isFileNameSafeForBaseDir('subdir/../../../etc/passwd', baseDir),
  76. ).toBe(false);
  77. });
  78. test('should return false for absolute path outside baseDir', () => {
  79. expect(isFileNameSafeForBaseDir('/etc/passwd', baseDir)).toBe(false);
  80. });
  81. test('should return false for path escaping to sibling directory', () => {
  82. expect(
  83. isFileNameSafeForBaseDir('subdir/../../other-dir/file.json', baseDir),
  84. ).toBe(false);
  85. });
  86. });
  87. describe('edge cases', () => {
  88. test('should handle empty filename', () => {
  89. // Empty filename resolves to baseDir itself, which is valid
  90. expect(isFileNameSafeForBaseDir('', baseDir)).toBe(true);
  91. });
  92. test('should handle . (current directory)', () => {
  93. expect(isFileNameSafeForBaseDir('.', baseDir)).toBe(true);
  94. });
  95. test('should handle ./filename', () => {
  96. expect(isFileNameSafeForBaseDir('./test.json', baseDir)).toBe(true);
  97. });
  98. });
  99. });
  100. describe('assertFileNameSafeForBaseDir', () => {
  101. const baseDir = '/tmp/growi-export';
  102. describe('valid file names (should not throw)', () => {
  103. test('should not throw for simple filename', () => {
  104. expect(() => {
  105. assertFileNameSafeForBaseDir('test.json', baseDir);
  106. }).not.toThrow();
  107. });
  108. test('should not throw for filename in subdirectory', () => {
  109. expect(() => {
  110. assertFileNameSafeForBaseDir('subdir/test.json', baseDir);
  111. }).not.toThrow();
  112. });
  113. });
  114. describe('path traversal attacks (should throw)', () => {
  115. test('should throw for ../etc/passwd', () => {
  116. expect(() => {
  117. assertFileNameSafeForBaseDir('../etc/passwd', baseDir);
  118. }).toThrow('Invalid file path: path traversal detected');
  119. });
  120. test('should throw for ../../etc/passwd', () => {
  121. expect(() => {
  122. assertFileNameSafeForBaseDir('../../etc/passwd', baseDir);
  123. }).toThrow('Invalid file path: path traversal detected');
  124. });
  125. test('should throw for absolute path outside baseDir', () => {
  126. expect(() => {
  127. assertFileNameSafeForBaseDir('/etc/passwd', baseDir);
  128. }).toThrow('Invalid file path: path traversal detected');
  129. });
  130. test('should throw for path escaping to sibling directory', () => {
  131. expect(() => {
  132. assertFileNameSafeForBaseDir(
  133. 'subdir/../../other-dir/file.json',
  134. baseDir,
  135. );
  136. }).toThrow('Invalid file path: path traversal detected');
  137. });
  138. });
  139. });
  140. });