Skip to content

Commit be33cbc

Browse files
committed
Harden 85acab9
1 parent 634c8ed commit be33cbc

File tree

2 files changed

+64
-11
lines changed

2 files changed

+64
-11
lines changed

index.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,33 @@ import {isStream} from 'is-stream';
99

1010
const pipeline = promisify(stream.pipeline); // TODO: Use `node:stream/promises` when targeting Node.js 16.
1111

12-
// Use `path.basename()` to prevent path traversal.
13-
const getPath = (prefix = '') => path.join(tempDir, path.basename(prefix) + uniqueString());
12+
// TODO: Use `is-safe-filename` when targeting Node.js 20.
13+
function assertSafePathComponent(pathComponent) {
14+
if (typeof pathComponent !== 'string') {
15+
throw new TypeError(`Expected a string, got ${typeof pathComponent}`);
16+
}
17+
18+
const trimmed = pathComponent.trim();
19+
20+
const isSafe = trimmed !== ''
21+
&& trimmed !== '.'
22+
&& trimmed !== '..'
23+
&& !pathComponent.includes('/')
24+
&& !pathComponent.includes('\\')
25+
&& !pathComponent.includes('\0');
26+
27+
if (!isSafe) {
28+
throw new Error(`Unsafe path component: ${JSON.stringify(pathComponent)}`);
29+
}
30+
}
31+
32+
const getPath = (prefix = '') => {
33+
if (prefix) {
34+
assertSafePathComponent(prefix);
35+
}
36+
37+
return path.join(tempDir, prefix + uniqueString());
38+
};
1439

1540
const writeStream = async (filePath, data) => pipeline(data, fs.createWriteStream(filePath));
1641

@@ -23,17 +48,21 @@ async function runTask(temporaryPath, callback) {
2348
}
2449

2550
export function temporaryFile({name, extension} = {}) {
26-
if (name) {
51+
if (name !== undefined && name !== null) {
2752
if (extension !== undefined && extension !== null) {
2853
throw new Error('The `name` and `extension` options are mutually exclusive');
2954
}
3055

31-
// Use `path.basename()` to prevent path traversal.
32-
return path.join(temporaryDirectory(), path.basename(name));
56+
assertSafePathComponent(name);
57+
58+
return path.join(temporaryDirectory(), name);
59+
}
60+
61+
if (extension !== undefined && extension !== null) {
62+
assertSafePathComponent(extension);
3363
}
3464

35-
// Use `path.basename()` to prevent path traversal.
36-
return getPath() + (extension === undefined || extension === null ? '' : '.' + path.basename(extension).replace(/^\./, ''));
65+
return getPath() + (extension === undefined || extension === null ? '' : '.' + extension.replace(/^\./, ''));
3766
}
3867

3968
export const temporaryFileTask = async (callback, options) => runTask(temporaryFile(options), callback);

test.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,32 @@ test('.root', t => {
141141
t.true(path.isAbsolute(rootTemporaryDirectory));
142142
});
143143

144-
test('path traversal prevention', t => {
145-
t.true(temporaryFile({name: '../../../test'}).includes(tempDir));
146-
t.true(temporaryFile({extension: '../../../test'}).includes(tempDir));
147-
t.true(temporaryDirectory({prefix: '../../../test'}).includes(tempDir));
144+
// TODO: Use `unsafeFilenameFixtures` from `is-safe-filename` when targeting Node.js 20.
145+
const unsafeFilenameFixtures = [
146+
'',
147+
' ',
148+
'.',
149+
'..',
150+
' .',
151+
'. ',
152+
' ..',
153+
'.. ',
154+
'../',
155+
'../foo',
156+
'foo/../bar',
157+
'foo/bar',
158+
'foo\\bar',
159+
'foo\0bar',
160+
];
161+
162+
test('rejects unsafe path components', t => {
163+
for (const fixture of unsafeFilenameFixtures) {
164+
t.throws(() => temporaryFile({name: fixture}), {message: /Unsafe path component/});
165+
t.throws(() => temporaryFile({extension: fixture}), {message: /Unsafe path component/});
166+
167+
// Empty string is valid for prefix (it's the default)
168+
if (fixture !== '') {
169+
t.throws(() => temporaryDirectory({prefix: fixture}), {message: /Unsafe path component/});
170+
}
171+
}
148172
});

0 commit comments

Comments
 (0)