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 +11 -0
- package/README.md +14 -6
- package/dist/index.d.ts +1 -9
- package/dist/index.js +91 -71
- package/package.json +3 -3
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
|
-
|
|
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
|
|
25
|
-
// But the user should not be able to access
|
|
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 = '
|
|
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/
|
|
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(
|
|
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: /%
|
|
4
|
+
regex: /%2[eE]/g,
|
|
6
5
|
replacement: '.',
|
|
7
6
|
},
|
|
8
7
|
{
|
|
9
|
-
regex: /%
|
|
8
|
+
regex: /%2[fF]/g,
|
|
10
9
|
replacement: '/',
|
|
11
10
|
},
|
|
12
11
|
{
|
|
13
|
-
regex: /%
|
|
12
|
+
regex: /%5[cC]/g,
|
|
14
13
|
replacement: '\\',
|
|
15
14
|
},
|
|
16
15
|
],
|
|
17
|
-
|
|
18
|
-
notAllowedRegEx: /:|\$|!|'|"|@|\+|`|\||=/g,
|
|
16
|
+
notAllowedRegEx: /:|\$|!|'|"|@|\+|`|\||=|\u0000/g,
|
|
19
17
|
};
|
|
20
18
|
/**
|
|
21
|
-
*
|
|
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
|
-
|
|
23
|
+
function validateOptions(options) {
|
|
24
24
|
if (!options)
|
|
25
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Replace
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
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": "
|
|
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.
|
|
30
|
+
"@types/node": "^25.9.9",
|
|
31
31
|
"typescript": "^5.9.3",
|
|
32
|
-
"vitest": "^4.
|
|
32
|
+
"vitest": "^4.1.12"
|
|
33
33
|
}
|
|
34
34
|
}
|