Between January 28 and March 10, 2026, node-tar published three High-severity advisories. Each one lets a crafted archive write files outside the folder you extract into, during an ordinary tar.x(). The fixes for these three advisories are all included in tar 7.5.11. That is the minimum version discussed here, not a claim that it is the latest release.
The version bump fixes the dependency. To decide what to fix first, you also need to know which code extracts archives and where those archives come from. On November 2, 2026, we ran that search across 167 repositories with an agent connected to the Codemod MCP. What you'll take away from this post:
- A version scan tells you what's installed, not what's reachable.
- Structural search separates tar extract calls from unrelated matches.
- Who calls the code and where the archive comes from help decide what to fix first.
- You can run the same searches with your own agent and the Codemod MCP.
This is how the confirmed source findings narrowed at each step.
| Step | What the search looked for | Result |
|---|---|---|
| 1 | Files that import tar | 19 files in 8 repos |
| 2 | Calls that extract an archive | 11 calls in 9 files across 5 repos |
| 3 | Calls confirmed on tar below 7.5.11 | 3 calls in 2 repos |
| 4 | Confirmed affected calls using user-chosen archives | 1 call in this source snapshot |
Our test bed is Codemod's GitHub organization: 175 repositories, of which 8 are empty. It holds our own code alongside copies of open-source projects such as Backstage, Strapi and tldraw.
These are confirmed findings in indexed source. Unresolved versions still need investigation, and a later artifact audit found a legacy CLI consumer that this search missed.
Three link-handling bugs let archives write outside the extraction folder
| Advisory | Published | CVSS v3.1 | First fixed in |
|---|---|---|---|
CVE-2026-24842, hardlink path traversal | Jan 28, 2026 | 8.2 High | 7.5.7 |
CVE-2026-29786, hardlink via drive-relative linkpath | Mar 5, 2026 | 7.1 High | 7.5.10 |
CVE-2026-31802, symlink via drive-relative linkpath | Mar 10, 2026 | 7.1 High | 7.5.11 |
All three involve link entries. A hardlink or symlink inside the archive points somewhere the extractor should never write, and the check that should stop it resolves paths differently from the code that creates the link.
The prompt asks for evidence beyond package names
Using the Codemod MCP, find every place across our repositories that extracts a tar archive with the tar package: tar.x, tar.extract, or a bare x() imported from tar. For each one, give me the repository, the file and the resolved tar version from the lockfile. Then find who calls each one and where the archive comes from. Tell me which repositories you searched and which you skipped.
We gave this prompt to Claude Code with the Codemod MCP connected. It turned the prompt into four searches, followed by a review of mitigations. Below are the rules and trimmed output.
1. Exact tar imports narrow the search to 19 files
A text search for tar also matches tar-stream, tarball, comments and variable names. The agent searched for the import itself:
id: tar-import
language: typescript
rule:
any:
- kind: import_statement
has:
field: source
regex: '^[''"]tar[''"]$'
- pattern: "require('tar')"
- pattern: 'require("tar")'Search {"countComplete":true,"totalMatches":20,"totalFiles":19,
"repositoriesSearched":167,"filesScanned":12184}
Skipped {"empty_repository":8}
codemod/commons tooling/utilities/src/functions/archive.ts
3: import * as tar from "tar";
4: import * as tarStream from "tar-stream";
codemod/backstage packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.ts
27: import * as tar from 'tar';The rule matches line 3 of the commons file and skips line 4, because the module name has to be exactly tar.
The first page stopped at the scan budget after 9,135 files, with 13 files found, and said so with countComplete: false. An earlier text search for the same imports also stopped at the scan budget. Continuing with the cursor finished the scan at 12,184 files and 19 files in 8 repositories. One of the 19 is a TypeScript type declaration, so 18 contain code.
2. Matching imported names removes two extraction false positives
Show the complete tar extraction search rule
id: tar-extract
language: TypeScript
rule:
any:
- all:
- matches: tar-member-call
- inside:
kind: program
stopBy: end
has:
matches: tar-object-import
- all:
- matches: tar-direct-call
- inside:
kind: program
stopBy: end
has:
matches: tar-named-import
utils:
tar-import:
kind: import_statement
has:
field: source
pattern:
context: "'tar'"
strictness: relaxed
tar-member-call:
any:
- pattern: $T.x($$$ARGS)
- pattern: $T.extract($$$ARGS)
tar-direct-call:
pattern: $FN($$$ARGS)
tar-object-import:
all:
- matches: tar-import
- has:
kind: import_clause
has:
any:
- all:
- kind: identifier
- pattern: $T
- all:
- kind: namespace_import
- has:
kind: identifier
pattern: $T
tar-named-import:
all:
- matches: tar-import
- has:
kind: import_specifier
stopBy: end
all:
- has:
field: name
regex: "^(x|extract)$"
- any:
- has:
field: alias
pattern: $FN
- all:
- not:
has:
field: alias
kind: identifier
- has:
field: name
pattern: $FNA search for x( on its own would match thousands of unrelated calls. This rule constrains the call name to a tar import in the same file. With import * as tar from 'tar', it matches tar.x() and tar.extract(). With a named import, it matches the imported name, including an alias like import { x as untar } from 'tar'.
Search {"countComplete":true,"totalMatches":11,"totalFiles":9,
"repositoriesSearched":167,"filesScanned":64122}
codemod/commons tooling/utilities/src/functions/archive.ts
38: return tar.x({ file: source, cwd: target });
codemod/strapi packages/cli/create-strapi-app/src/utils/template.ts
162: tar.x({
codemod/backstage packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.ts
175: tar.extract({
codemod/backstage packages/cli-module-build/src/lib/packager/createDistWorkspace.ts
311: await tar.extract({ file: archivePath, cwd: targetDir, strip: 1 });That is 11 extract calls in 9 files across 5 repositories: Backstage, commons, tldraw, Strapi and a popular project scaffolder, with no manual filtering. A looser first version of the rule matched any extract() call in a file that imports tar. It returned 13 matches, and two came from other libraries in the commons file: d.extract from unzipper and tarStream.extract from tar-stream. We had to drop those by hand. This rule skips them because neither object is the tar import.

The search needed three cursor continuations before it finished at 64,122 files.
Step 1 found no require('tar') calls in these repositories, so following imports was enough here.
The other 3 repositories from step 1 import tar without extracting anything. Two call tar.create and one calls tar.list.
3. Lockfile versions confirm three calls on affected tar copies
The agent read resolved versions from each repository's lockfile through the dependency index. This is the output for the scaffolder's repository, with package names replaced:
packageName version ecosystem lockFilePath depth workspacePackage
tar 7.4.3 npm pnpm-lock.yaml 0 (scaffolder package)
tar 7.4.3 npm pnpm-lock.yaml 0 (another workspace package)
tar 0.4.43 cargo Cargo.lock 3 (a Rust dependency)A grep of lockfiles for tar returns all three rows plus every tar-stream and tar-fs entry. The dependency index keeps the npm package apart from a Rust crate with the same name and says which workspace uses it.
| Repository | tar version | Status |
|---|---|---|
| Backstage | 7.5.22 | Fixed |
| Strapi | 7.5.22 | Fixed |
| tldraw | 7.5.13 (transitive) | Transitive copy fixed; own imports unresolved. |
| Project scaffolder (our copy) | 7.4.3 | Affected by all three |
| commons | 6.2.1 | Affected by all three |
That confirms 3 extract calls on an affected version: 2 in the scaffolder and 1 in commons. The unresolved version behind tldraw's own imports is not included in that count and has not been cleared.
This table is the November 2 source snapshot. After the searches, commons PR #89: Bump tar to 7.5.11 in utilities merged, changing its locked tar from 6.2.1 to 7.5.11. That source change does not replace separately published bundles.
4. Caller tracing identifies one user-selected-template path
In commons, the call sits in a helper that wraps tar.x({ file: source, cwd: target }) and exports it as untar. A version-only scan flags commons because its lockfile resolves tar to 6.2.1. Claude Code used Codemod MCP to search all 167 indexed repositories for untar( and found no callers in that source snapshot. That did not establish unreachability: the published legacy CLI was later found to bundle and invoke a separate historical copy of the utilities helper.
In the scaffolder, the agent searched for callers of the two functions that contain the extract calls. Both are called from the CLI's main flow.
One always downloads the scaffolder's own repository. The other downloads whatever GitHub repository the user passes as a template and extracts it. This is that function, simplified and with names changed:
async function fetchTemplate(root, { owner, repo, branch, subdir }) {
await pipeline(
await download(`https://codeload.github.com/${owner}/${repo}/tar.gz/${branch}`),
x({
cwd: root,
strip: 1,
// keeps entries whose path is inside the requested folder
filter: (entryPath) => entryPath.startsWith(`${topDir}/${subdir}/`),
}),
);
}That is the 1 user-selected-template call identified at the bottom of this source-analysis funnel. The archive comes from a repository the user names, and the extract runs on tar 7.4.3.
Caller searches match names in text, so a call through a re-export or a renamed import could still slip past.
Within the November 2 indexed-source snapshot, Claude Code + Codemod MCP identified the scaffolder's user-selected-template path as the higher-priority remediation target based on the callers and archive sources it found. That prioritization was not proof that commons had no shipped consumers or that the scaffolder was exploitable.
5. Entry-path filters do not validate link targets
The scaffolder's filter decides which entries to extract by their path. It doesn't look at where a link entry points.
Backstage's archive reader does. Its filter rejects any symlink or hardlink whose target resolves outside the extraction folder:
// Block symlinks/hardlinks that escape the extraction directory
const entry = stat as ReadEntry;
if ((entry.type === "SymbolicLink" || entry.type === "Link") && entry.linkpath) {
const strippedPath = path.split("/").slice(strip).join("/");
const linkDir = platformPath.dirname(platformPath.join(dir, strippedPath));
const targetPath = platformPath.resolve(linkDir, entry.linkpath);
if (!isChildPath(dir, targetPath)) {
return false;
}
}This check lives in the extract call, so a lockfile scan can't see it.
We have not tested whether the scaffolder's call can be exploited. GitHub builds these tarballs from the repository's files. A git repository can hold symlinks but not hardlinks, so the two hardlink advisories may not apply to this path.
Our copy of the scaffolder is older than its current upstream version, so these results describe our copy only.
Source searches are not release audits
The published legacy CLI bundled and invoked a separate historical copy of the utilities helper, outside the indexed-source snapshot.
In the follow-up artifact audit, the published codemod@legacy release, 0.18.13, contained both the utilities extraction wrapper and the CLI path that calls it on downloaded registry archives. The matching release-tag lockfile resolves tar to 6.2.1. The exact version comes from that lockfile, not an embedded SBOM or a reproducible-build proof.
CLI PR #2465: Fix legacy CLI bundled tar archive extraction vulnerabilities merged on November 3, updating the legacy CLI's separate utilities copy to tar 7.5.11. As of November 4, npm still resolves codemod@legacy to 0.18.13; the merged fix is not yet available through that tag. Users receive the fix only after an approved patch release and an update to the legacy dist-tag. The merged commons change alone does not establish that availability.
Call-site evidence does not establish exploitability
- Confirmed: 19 importing files, 11 extract calls, and the tar versions for Backstage, Strapi, commons and our copy of the scaffolder.
- Unresolved: the tar version behind tldraw's own imports, additional callers reached through re-exports or dynamic imports, and whether the scaffolder's call is exploitable.
- Release boundary: the original source search did not inventory published bundles. The follow-up legacy CLI finding needs a separate approved release; a merged source fix is not proof that users have received it.
- Coverage: 167 of 175 repositories searched, 8 empty ones skipped, and the import scan finished at 12,184 files.
A fix has to reach the artifacts users run
- Upgrade affected tar copies to a maintained release that includes the fixes. Version 7.5.11 is the floor for the three advisories examined here, not a substitute for checking newer advisories. Tar 6 to 7 is a major version, so read the changelog.
- Find every extract call as well as every lockfile entry. You can try the step 2 rule on public code in Codemod Grep.
- Resolve the version per call site, including transitive copies. An agent with the Codemod MCP can read versions from lockfiles and trace callers across your repositories.
- Find out where each archive comes from. A user-supplied archive comes first.
- Review link-target validation, not just entry-path filters. Reject or constrain link entries according to your extraction policy as defense in depth, not as a replacement for upgrading tar.
- Re-run the searches after the bump and inspect rebuilt distributables. Record versions, coverage, unresolved cases and release status separately.
Your agent can trace the same path with Codemod MCP
To run the same searches on your own repositories:
- Connect the Codemod MCP to your agent. The setup guide covers Claude Code, Cursor and other clients. You sign in with OAuth, so there's no API key to manage. It's free to try.
- Give your agent the prompt from this post, or start from the rules in steps 1 and 2.
- Ask it to trace callers and versions for every match, and to list the repositories it searched and skipped.
To try structural search on public code first, with nothing to connect, open grep.codemod.com.
To run this across your organization's repositories, book a demo.