path-sanitizer 4.0.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## [5.0.0] - 2026-10-31
4
+
5
+ ### Fixed
6
+
7
+ - [GHSA-v6jx-8m6q-xm2r](https://github.com/cabraviva/path-sanitizer/security/advisories/GHSA-v6jx-8m6q-xm2r)
8
+
9
+ ### Breaking Changes
10
+
11
+ - Removed `SanitizeOptions.parentDirectoryRegEx`, as it's now always enforced internally
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A simple lightweight npm package for sanitizing paths to prevent Path Traversal
4
4
 
5
- # Usage
5
+ ## Usage
6
6
 
7
7
  ```js
8
8
  import sanitize from 'path-sanitizer'
@@ -21,11 +21,11 @@ import path from 'path'
21
21
  // Real scenario:
22
22
  function routeHandler(myPath) {
23
23
  // Lets just assume that the path was extracted from the request
24
- // We want to read a file in the C:\Users\user\Desktop\myApp\ directory
25
- // But the user should not be able to access C:\Users\user\Desktop\
24
+ // We want to read a file in the /home/user/server/static/ directory
25
+ // But the user should not be able to access /home/user/server/
26
26
  // So we need to sanitize the path
27
27
 
28
- const APP_DIR = 'C:\\Users\\user\\Desktop\\myApp\\'
28
+ const APP_DIR = '/home/user/server/static/'
29
29
  const sanitized = path.join(APP_DIR, sanitize(myPath))
30
30
 
31
31
  // Now we would usually read the file
@@ -34,9 +34,17 @@ function routeHandler(myPath) {
34
34
  }
35
35
 
36
36
  routeHandler('my-video.mp4') // Just fine...
37
- routeHandler('../../Documents/grades.docx') // But what if we try to access a file outside the app directory?
38
- routeHandler('/hello.txt') // What if the path starts with a slash, and we try to use an absolute path?
37
+ routeHandler('../../Documents/personal-file.pdf') // But what if we try to access a file outside the app directory?
38
+ routeHandler('/home/user/hello.txt') // What if the path starts with a slash, and we try to use an absolute path?
39
39
  routeHandler('////////a\\\\../hi.txt') // What if the path is not valid?
40
40
  routeHandler(500) // What if the path is not a string?
41
41
  routeHandler('/:11^^') // What if the path contains not allowed characters (like ":")?
42
42
  ```
43
+
44
+ ## Note
45
+
46
+ This package is optimized for linux environments. You can try to use it on Windows, and it will mostly work fine, but it is recommended to use on a linux system or inside a docker container.
47
+
48
+ ## Contributing
49
+
50
+ If you have any issues with `path-sanitizer` or you have discovered a vulnerability / bypass, please make sure to report it. I appreciate every contribution.
package/dist/index.d.ts CHANGED
@@ -3,10 +3,6 @@ export interface SanitizeOptions {
3
3
  regex: RegExp;
4
4
  replacement: string;
5
5
  }[];
6
- /**
7
- * RegEx that filters out stuff like \..\ or /../
8
- */
9
- parentDirectoryRegEx?: RegExp;
10
6
  /**
11
7
  * Characters to be removed from paths because the file system doesn't support these.
12
8
  */
@@ -17,10 +13,6 @@ interface SanitizeOptionsComplete {
17
13
  regex: RegExp;
18
14
  replacement: string;
19
15
  }[];
20
- /**
21
- * RegEx that filters out stuff like \..\ or /../
22
- */
23
- parentDirectoryRegEx: RegExp;
24
16
  /**
25
17
  * Characters to be removed from paths because the file system doesn't support these.
26
18
  */
@@ -30,5 +22,5 @@ export declare const DEFAULT_OPTIONS: SanitizeOptionsComplete;
30
22
  /**
31
23
  * Sanitizes a portion of a path to avoid Path Traversal
32
24
  */
33
- export default function sanitize(pathstr: string, options?: SanitizeOptions): string;
25
+ export default function sanitize(pathString: string, options?: SanitizeOptions): string;
34
26
  export {};
package/dist/index.js CHANGED
@@ -1,93 +1,113 @@
1
- import { normalize, join } from 'node:path';
2
1
  export const DEFAULT_OPTIONS = {
3
2
  decode: [
4
3
  {
5
- regex: /%2e/g,
4
+ regex: /%2[eE]/g,
6
5
  replacement: '.',
7
6
  },
8
7
  {
9
- regex: /%2f/g,
8
+ regex: /%2[fF]/g,
10
9
  replacement: '/',
11
10
  },
12
11
  {
13
- regex: /%5c/g,
12
+ regex: /%5[cC]/g,
14
13
  replacement: '\\',
15
14
  },
16
15
  ],
17
- parentDirectoryRegEx: /[\/\\]\.\.[\/\\]/g,
18
- notAllowedRegEx: /:|\$|!|'|"|@|\+|`|\||=/g,
16
+ notAllowedRegEx: /:|\$|!|'|"|@|\+|`|\||=|\u0000/g,
19
17
  };
20
18
  /**
21
- * Sanitizes a portion of a path to avoid Path Traversal
19
+ * Internal utility function that validates the SanitizeOptions and fills them with DEFAULT_OPTIONS unless already complete.
20
+ * @param options (incomplete) SanitizeOptions
21
+ * @returns Fully valid SanitizeOptionsComplete
22
22
  */
23
- export default function sanitize(pathstr, options = DEFAULT_OPTIONS) {
23
+ function validateOptions(options) {
24
24
  if (!options)
25
- options = DEFAULT_OPTIONS;
26
- if (typeof options !== 'object')
25
+ return DEFAULT_OPTIONS;
26
+ if (typeof options !== 'object') {
27
27
  throw new Error('options must be an object');
28
- if (!Array.isArray(options.decode))
29
- options.decode = DEFAULT_OPTIONS.decode;
30
- if (!options.parentDirectoryRegEx)
31
- options.parentDirectoryRegEx = DEFAULT_OPTIONS.parentDirectoryRegEx;
32
- if (!options.notAllowedRegEx)
33
- options.notAllowedRegEx = DEFAULT_OPTIONS.notAllowedRegEx;
34
- if (typeof pathstr !== 'string') {
35
- // Stringify the path
36
- pathstr = `${pathstr}`;
37
28
  }
38
- let sanitizedPath = pathstr;
39
- // ################################################################################################################
40
- // Decode
41
- options.decode.forEach((decode) => {
42
- sanitizedPath = sanitizedPath.replace(decode.regex, decode.replacement);
43
- });
44
- // Remove not allowed characters
45
- sanitizedPath = sanitizedPath.replace(options.notAllowedRegEx, '');
46
- // Replace backslashes with normal slashes
47
- sanitizedPath = sanitizedPath.replace(/[\\]/g, '/');
48
- // Replace /../ with /
49
- sanitizedPath = sanitizedPath.replace(options.parentDirectoryRegEx, '/');
50
- // Remove ../ at pos 0 and /.. at end
51
- sanitizedPath = sanitizedPath.replace(/^\.\.[\/\\]/g, '/');
52
- sanitizedPath = sanitizedPath.replace(/[\/\\]\.\.$/g, '/');
53
- // Replace double (back)slashes with a single slash
54
- sanitizedPath = sanitizedPath.replace(/[\/\\]+/g, '/');
55
- // Normalize path
56
- sanitizedPath = normalize(sanitizedPath);
57
- // Remove / or \ in the end
58
- while (sanitizedPath.endsWith('/') || sanitizedPath.endsWith('\\')) {
59
- sanitizedPath = sanitizedPath.slice(0, -1);
29
+ return {
30
+ decode: Array.isArray(options.decode)
31
+ ? options.decode
32
+ : DEFAULT_OPTIONS.decode,
33
+ notAllowedRegEx: options.notAllowedRegEx ?? DEFAULT_OPTIONS.notAllowedRegEx,
34
+ };
35
+ }
36
+ /**
37
+ * Internal utility function to normalize slashes in a path (e.g. from '\' to '/')
38
+ * @param val path string
39
+ * @returns slash-normalized path
40
+ */
41
+ function normalizeSlashes(val) {
42
+ // Replace backslashes to forward slashes
43
+ val = val.replaceAll('\\', '/');
44
+ // Replace all multi combined slashes into one
45
+ val = val.replace(/\/+/g, '/');
46
+ return val;
47
+ }
48
+ /**
49
+ * Internal utility function to apply all decode regexes from SanitizeOptions.decode to a path string
50
+ * @param pathstr a path string
51
+ * @param decodeOptions valid SanitizeOptionsComplete.decode
52
+ * @returns a decoded path string
53
+ */
54
+ function decodePath(pathstr, decodeOptions) {
55
+ for (const decodeOption of decodeOptions) {
56
+ pathstr = pathstr.replace(decodeOption.regex, decodeOption.replacement);
60
57
  }
61
- // Remove / or \ in the beginning
62
- while (sanitizedPath.startsWith('/') || sanitizedPath.startsWith('\\')) {
63
- sanitizedPath = sanitizedPath.slice(1);
58
+ return pathstr;
59
+ }
60
+ /**
61
+ * Internal utility function to ensure the specified notAllowedRegEx
62
+ */
63
+ function removeInvalidCharacters(pathstr, notAllowedRegEx) {
64
+ return pathstr.replace(notAllowedRegEx, '');
65
+ }
66
+ /**
67
+ * Internal function which takes a path string and ensures every segment is valid
68
+ */
69
+ function applyPathFilters(pathString) {
70
+ const filteredPath = pathString
71
+ .trim() // trim the whole path-string
72
+ .split('/') // split into path-segments
73
+ .filter((segment) => {
74
+ const trimmedSegment = segment.trim(); // trim every segment
75
+ return (trimmedSegment !== '' &&
76
+ trimmedSegment !== '.' &&
77
+ trimmedSegment !== '..'); // return whether the segment is a valid path-portion (no '', '.' or '..')
78
+ // if valid => keep; invalid => remove
79
+ })
80
+ .join('/') // reconstruct path string
81
+ .trim(); // trim again
82
+ return /^\.+$/.test(filteredPath) ? '' : filteredPath; // ensures the path string is not just dots
83
+ }
84
+ /**
85
+ * Failsafe helper function to catch all remaining absolute paths (not wanted by sanitize())
86
+ * works by ensuring no '/' at start or end and no '..' in a segment in-between
87
+ */
88
+ function isSafeLinuxRelativePath(pathString) {
89
+ return (!pathString.startsWith('/') &&
90
+ !pathString.endsWith('/') &&
91
+ !pathString.split('/').some((segment) => segment === '..'));
92
+ }
93
+ /**
94
+ * Sanitizes a portion of a path to avoid Path Traversal
95
+ */
96
+ export default function sanitize(pathString, options = DEFAULT_OPTIONS) {
97
+ // Validate options and set default options if user did not set
98
+ const validOptions = validateOptions(options);
99
+ // Validate path is a string, otherwise convert to string
100
+ if (typeof pathString !== 'string') {
101
+ pathString = `${pathString}`;
64
102
  }
65
- // Validate path
66
- sanitizedPath = join('', sanitizedPath);
67
- // Remove not allowed characters
68
- sanitizedPath = sanitizedPath.replace(options.notAllowedRegEx, '');
69
- // Again change all \ to /
70
- sanitizedPath = sanitizedPath.replace(/[\\]/g, '/');
71
- // Replace double (back)slashes with a single slash
72
- sanitizedPath = sanitizedPath.replace(/[\/\\]+/g, '/');
73
- // Replace /../ with /
74
- sanitizedPath = sanitizedPath.replace(options.parentDirectoryRegEx, '/');
75
- // Remove ./ or / at start
76
- while (sanitizedPath.startsWith('/') ||
77
- sanitizedPath.startsWith('./') ||
78
- sanitizedPath.endsWith('/..') ||
79
- sanitizedPath.endsWith('/../') ||
80
- sanitizedPath.startsWith('../') ||
81
- sanitizedPath.startsWith('/../')) {
82
- sanitizedPath = sanitizedPath.replace(/^\.\//g, ''); // ^./
83
- sanitizedPath = sanitizedPath.replace(/^\//g, ''); // ^/
84
- // Remove ../ | /../ at pos 0 and /.. | /../ at end
85
- sanitizedPath = sanitizedPath.replace(/^[\/\\]\.\.[\/\\]/g, '/');
86
- sanitizedPath = sanitizedPath.replace(/^\.\.[\/\\]/g, '/');
87
- sanitizedPath = sanitizedPath.replace(/[\/\\]\.\.$/g, '/');
88
- sanitizedPath = sanitizedPath.replace(/[\/\\]\.\.\/$/g, '/');
103
+ if (pathString.length > 4096) {
104
+ throw new Error(`filename is over - 4096 characters`);
89
105
  }
90
- // Make sure out is not "." or ".." or any amount of "only dots"
91
- sanitizedPath = sanitizedPath.trim().replace(/^(\.*)$/g, '');
92
- return sanitizedPath.trim();
106
+ // Apply every configurable transformation before traversal filtering.
107
+ pathString = decodePath(pathString, validOptions.decode);
108
+ pathString = removeInvalidCharacters(pathString, validOptions.notAllowedRegEx);
109
+ pathString = normalizeSlashes(pathString);
110
+ pathString = applyPathFilters(pathString);
111
+ // No path transformation may happen after this final safety check.
112
+ return isSafeLinuxRelativePath(pathString) ? pathString : '';
93
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-sanitizer",
3
- "version": "4.0.3",
3
+ "version": "5.0.0",
4
4
  "description": "A simple lightweight npm package for sanitizing paths to prevent Path Traversal",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "homepage": "https://github.com/cabraviva/path-sanitizer#readme",
29
29
  "devDependencies": {
30
- "@types/node": "^25.3.2",
30
+ "@types/node": "^25.9.9",
31
31
  "typescript": "^5.9.3",
32
- "vitest": "^4.0.18"
32
+ "vitest": "^4.1.12"
33
33
  }
34
34
  }