Skip to content

Commit ce787f3

Browse files
committed
Reject non-string prefix, postfix, template
The relative-value guard in `_assertPath` calls `.includes('..')` directly on the user-supplied value. When the value is an Array the call checks element equality (so `['../escape'].includes('..')` is false), and when the value is an arbitrary object a duck-typed `includes` returning false defeats the check entirely. In both cases the value is subsequently coerced to a string by `Array.prototype.join` inside `_generateTmpName` and by `path.join`, so a non-string carrying `../` still produces a path that escapes `tmpdir`. Tighten `_assertPath` to require `typeof value === 'string'` before the substring check, and apply the same type check to `template` ahead of the existing `XXXXXX` regex match (otherwise `match` throws on a non-string with an unrelated error). The error includes the option name so consumers can see which option was wrong. Adds a `test/GHSA-7c78-jf6q-g5cm-test.js` that exercises array, duck-typed object, and primitive (number) inputs across `fileSync`, `dirSync`, and `tmpNameSync`, and asserts that valid string inputs are still accepted. Signed-off-by: tonghuaroot <tonghuaroot@gmail.com>
1 parent 41f7159 commit ce787f3

2 files changed

Lines changed: 105 additions & 9 deletions

File tree

lib/tmp.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -526,16 +526,26 @@ function _generateTmpName(opts) {
526526
}
527527

528528
/**
529-
* Check the prefix and postfix options
529+
* Check the prefix, postfix, and template options.
530+
*
531+
* Rejects non-string inputs so that a non-string `.includes('..')` cannot evade
532+
* the substring check (e.g. an Array whose `.includes('..')` is element-wise,
533+
* or a duck-typed object with a custom `.includes`), and so that the value is
534+
* not later coerced to a string with traversal sequences via `Array.prototype.join`
535+
* or `path.join`.
530536
*
531537
* @private
532538
*/
533-
function _assertPath(path) {
534-
if (path.includes("..")) {
539+
function _assertPath(option, value) {
540+
if (typeof value !== 'string') {
541+
throw new Error(`${option} option must be a string, got "${typeof value}".`);
542+
}
543+
544+
if (value.includes("..")) {
535545
throw new Error("Relative value not allowed");
536546
}
537547

538-
return path;
548+
return value;
539549
}
540550

541551
/**
@@ -558,8 +568,13 @@ function _assertOptionsBase(options) {
558568
}
559569

560570
/* istanbul ignore else */
561-
if (!_isUndefined(options.template) && !options.template.match(TEMPLATE_PATTERN)) {
562-
throw new Error(`Invalid template, found "${options.template}".`);
571+
if (!_isUndefined(options.template)) {
572+
if (typeof options.template !== 'string') {
573+
throw new Error(`template option must be a string, got "${typeof options.template}".`);
574+
}
575+
if (!options.template.match(TEMPLATE_PATTERN)) {
576+
throw new Error(`Invalid template, found "${options.template}".`);
577+
}
563578
}
564579

565580
/* istanbul ignore else */
@@ -575,9 +590,9 @@ function _assertOptionsBase(options) {
575590
options.unsafeCleanup = !!options.unsafeCleanup;
576591

577592
// for completeness' sake only, also keep (multiple) blanks if the user, purportedly sane, requests us to
578-
options.prefix = _isUndefined(options.prefix) ? '' : _assertPath(options.prefix);
579-
options.postfix = _isUndefined(options.postfix) ? '' : _assertPath(options.postfix);
580-
options.template = _isUndefined(options.template) ? undefined : _assertPath(options.template);
593+
options.prefix = _isUndefined(options.prefix) ? '' : _assertPath('prefix', options.prefix);
594+
options.postfix = _isUndefined(options.postfix) ? '' : _assertPath('postfix', options.postfix);
595+
options.template = _isUndefined(options.template) ? undefined : _assertPath('template', options.template);
581596
}
582597

583598
/**

test/GHSA-7c78-jf6q-g5cm-test.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const assert = require('assert');
2+
const tmp = require('../lib/tmp');
3+
4+
describe('GHSA-7c78-jf6q-g5cm', function () {
5+
describe('#fileSync with non-string `prefix`', function () {
6+
it('should reject an array prefix even when its element is "../foo"', function (done) {
7+
assert.throws(function () {
8+
tmp.fileSync({ prefix: ['../foo'] });
9+
}, new RegExp('^Error: prefix option must be a string'));
10+
11+
done();
12+
});
13+
14+
it('should reject a duck-typed object whose includes() returns false', function (done) {
15+
assert.throws(function () {
16+
tmp.fileSync({
17+
prefix: { toString: function () { return '../foo'; }, includes: function () { return false; } }
18+
});
19+
}, new RegExp('^Error: prefix option must be a string'));
20+
21+
done();
22+
});
23+
24+
it('should reject a number prefix', function (done) {
25+
assert.throws(function () {
26+
tmp.fileSync({ prefix: 42 });
27+
}, new RegExp('^Error: prefix option must be a string'));
28+
29+
done();
30+
});
31+
});
32+
33+
describe('#fileSync with non-string `postfix`', function () {
34+
it('should reject an array postfix', function (done) {
35+
assert.throws(function () {
36+
tmp.fileSync({ postfix: ['/../foo'] });
37+
}, new RegExp('^Error: postfix option must be a string'));
38+
39+
done();
40+
});
41+
});
42+
43+
describe('#fileSync with non-string `template`', function () {
44+
it('should reject an array template', function (done) {
45+
assert.throws(function () {
46+
tmp.fileSync({ template: ['XXXXXX/../foo'] });
47+
}, new RegExp('^Error: template option must be a string'));
48+
49+
done();
50+
});
51+
});
52+
53+
describe('#dirSync with non-string `prefix`', function () {
54+
it('should reject an array prefix', function (done) {
55+
assert.throws(function () {
56+
tmp.dirSync({ prefix: ['../escape'] });
57+
}, new RegExp('^Error: prefix option must be a string'));
58+
59+
done();
60+
});
61+
});
62+
63+
describe('#tmpNameSync with non-string `prefix`', function () {
64+
it('should reject an array prefix', function (done) {
65+
assert.throws(function () {
66+
tmp.tmpNameSync({ prefix: ['../escape'] });
67+
}, new RegExp('^Error: prefix option must be a string'));
68+
69+
done();
70+
});
71+
});
72+
73+
describe('valid string prefixes still work', function () {
74+
it('should accept a normal string prefix', function (done) {
75+
const r = tmp.fileSync({ prefix: 'safe-prefix' });
76+
assert.ok(r.name.indexOf('safe-prefix') !== -1);
77+
r.removeCallback();
78+
done();
79+
});
80+
});
81+
});

0 commit comments

Comments
 (0)