Hosted npm `scan --vex` attests not_affected when the lock also installs the same name@version from a `file:` tarball or directory, which npm installs unpatched (tarball shape regressed in #1008)
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
Read crates/socket-patch-core/src/patch/redirect/mod.rs around the npm non_registry and link handling, then trace how bundled_skipped_uuids affects in-run VEX in crates/socket-patch-core/src/hosted/engine.rs. Reproduce both lockfile shapes described in the issue and compare the hosted scan with standalone vex. Done means the in-run VEX withholds the patch for both unredirected local copies, with warnings and attribution consistent with the documented contract.
索引モデルが issue の本文から書いたものです。
説明
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
scan --mode hosted --vex attests a package not_affected when the npm lock also installs that same name@version from a file: tarball or a file: directory. npm installs that copy from its spec, so it stays unpatched. A standalone vex run after npm ci refuses the same state correctly.
Two shapes, both in an npm workspace (install-strategy=nested) where member a depends on registry [email protected] and member b depends on a local copy of the same release:
b's spec |
scan warning | in-run --vex |
npm ci: a / b |
standalone vex after npm ci |
|---|---|---|---|---|
file:../../vendor/left-pad-1.3.0.tgz (tarball) |
redirect_npm_non_registry_entry_skipped |
not_affected |
patched / unpatched | exit 2, patched_ref_unattributable |
file:../../lpdir (directory, link: true) |
none | not_affected |
patched / unpatched | exit 1, not_applied (no_applicable_patches) |
This is the npm counterpart of the pnpm defect fixed for hosted pnpm in #1469 (refs #935). That PR touches only the pnpm path, so npm keeps the bug.
Impact
A scan --mode hosted --vex run, which is the documented one-shot CI flow, produces an OpenVEX not_affected statement while part of the app still loads the vulnerable bytes. The tarball shape at least prints a warning. The directory shape prints no warning at all. Both exit 0 with status: success.
Repro (Linux, npm 10.9.4 / Node 22.22.0, main 15c2c83)
You need a mock of the org patch API on 127.0.0.1:18765 serving one free patch for pkg:npm/[email protected], whose tarball prepends /*SOCKET_PATCHED*/ to index.js. The mock is in the details block below. It speaks the same contract as tests/e2e_redirect_npm_build.rs: patches/batch, patches/by-package, patches/package (granted, tarball artifact + sha512), patches/view/<uuid>, and the tarball route.
#!/usr/bin/env bash
# usage: repro.sh <npm-bin> <socket-patch-bin> <workdir> (mock API on :18765)
set -u; NPM=$1; SP=$2; W=$3; rm -rf "$W"; mkdir -p "$W/packages/a" "$W/packages/b" "$W/vendor"; cd "$W"
$NPM pack [email protected] --pack-destination vendor >/dev/null 2>&1
echo '{"name":"root","version":"0.0.0","private":true,"workspaces":["packages/*"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"file:../../vendor/left-pad-1.3.0.tgz"}}' > packages/b/package.json
# directory shape: unpack the tarball into ./lpdir and use "file:../../lpdir" instead
echo 'install-strategy=nested' > .npmrc
$NPM install --no-audit --no-fund >/dev/null 2>&1
$SP scan --mode hosted --json --yes --api-url http://127.0.0.1:18765 --org test-org --api-token fake \
--vex scan.vex.json --vex-product pkg:npm/[email protected] > scan.json; echo "scan exit=$?"
rm -rf node_modules packages/*/node_modules; $NPM ci --no-audit --no-fund >/dev/null 2>&1
head -c 20 packages/a/node_modules/left-pad/index.js; echo; head -c 20 packages/b/node_modules/left-pad/index.js; echo
$SP vex --json --output post.vex.json --product pkg:npm/[email protected] --patch-server-url http://127.0.0.1:18765 \
--api-url http://127.0.0.1:18765 --org test-org --api-token fake > vex.json; echo "vex exit=$?"
Output for the tarball shape, from the same script with a few extra prints of scan.json / scan.vex.json (npm 10.9.4; identical on npm 12.2.0):
scan exit=0
status success warnings ['redirect_npm_non_registry_entry_skipped', 'redirect_npm_allow_remote']
in-run VEX: [('pkg:npm/[email protected]', 'not_affected')]
npm ci exit=0
packages/a left-pad: PATCHED
packages/b left-pad: UNPATCHED
standalone vex exit=2
error ['patched_ref_unattributable']
The standalone vex warning reads: "package-lock.json: pkg:npm/[email protected] is wired to a Socket patch but lock entry packages/b/node_modules/left-pad is not installed from the registry (it resolves to "file:vendor/left-pad-1.3.0.tgz"); npm installs it from that spec, so that copy stays UNPATCHED and nothing is attested".
Directory shape: the scan has no redirect_npm_* skip warning, the in-run VEX is not_affected, b's copy is unpatched after npm ci, and the standalone vex omits the patch with not_applied.
mock.py (python3 mock.py cfg.json 18765)
import json, sys, re, hashlib, base64
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
# cfg: list of {name, version, uuid, tgz, files:{path:[before,after]}}
cfg = json.load(open(sys.argv[1])); port=int(sys.argv[2])
ORG="test-org"; TOKEN="22222222-2222-4222-8222-222222222222"
def purl(p): return f"pkg:npm/{p['name']}@{p['version']}"
def base(): return f"http://127.0.0.1:{port}"
def turl(p):
last=p['name'].split('/')[-1]
return f"{base()}/patch/npm/{p['name']}/{p['version']}/{TOKEN}/{p['uuid']}/{last}-{p['version']}.tgz"
def sri(p): return "sha512-"+base64.b64encode(hashlib.sha512(open(p['tgz'],'rb').read()).digest()).decode()
def summ(p): return {"uuid":p['uuid'],"purl":purl(p),"publishedAt":"2026-01-01T00:00:00Z","description":"x","license":"MIT","tier":"free","vulnerabilities":{},"cveIds":[],"ghsaIds":[],"severity":"high","title":"t"}
class H(BaseHTTPRequestHandler):
def log_message(self,*a): sys.stderr.write("REQ %s %s\n"%(self.command,self.path))
def j(self,o,code=200):
b=json.dumps(o).encode(); self.send_response(code); self.send_header("content-type","application/json"); self.send_header("content-length",str(len(b))); self.end_headers(); self.wfile.write(b)
def body(self):
n=int(self.headers.get('content-length',0)); return json.loads(self.rfile.read(n) or b'{}')
def do_POST(self):
path=self.path.split('?')[0]; b=self.body()
if path.endswith('/patches/batch'):
comps=[c.get('purl') if isinstance(c,dict) else c for c in b.get('components',b.get('purls',[]))]
pk=[{"purl":purl(p),"patches":[summ(p)]} for p in cfg if purl(p) in comps or not comps]
return self.j({"packages":pk,"canAccessPaidPatches":False})
if path.endswith('/patches/package'):
res={}
for p in cfg:
res[p['uuid']]={"status":"granted","url":turl(p),"purl":purl(p),"artifacts":[{"kind":"tarball","url":turl(p),"integrity":{"sha512":sri(p)}}],"registryOverride":None}
return self.j({"results":res})
self.j({"error":"nf"},404)
def do_GET(self):
path=self.path.split('?')[0]
m=re.search(r'/patches/by-package/(.+)$',path)
if m:
from urllib.parse import unquote
q=unquote(m.group(1))
return self.j({"patches":[summ(p) for p in cfg if purl(p)==q or q.startswith(purl(p).rsplit('@',1)[0])],"canAccessPaidPatches":False})
m=re.search(r'/patches/view/([^/]+)$',path)
if m:
for p in cfg:
if p['uuid']==m.group(1):
d=summ(p); d["files"]={k:{"beforeHash":v[0],"afterHash":v[1]} for k,v in p['files'].items()}
d["vulnerabilities"]={"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2026-1111"],"summary":"s","severity":"high","description":"d"}}
return self.j(d)
for p in cfg:
if path==turl(p)[len(base()):]:
b=open(p['tgz'],'rb').read(); self.send_response(200); self.send_header("content-length",str(len(b))); self.end_headers(); self.wfile.write(b); return
self.j({"error":"nf"},404)
ThreadingHTTPServer(("127.0.0.1",port),H).serve_forever()
cfg.json: [{"name":"left-pad","version":"1.3.0","uuid":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","tgz":"<patched left-pad-1.3.0.tgz>","files":{"package/index.js":["<git-sha256 before>","<git-sha256 after>"]}}]
Expected vs actual
Expected. The in-run VEX withholds a patch whose name@version also installs from a copy the run can't redirect. Three things in the repo say so:
CLI_CONTRACT.md, the attribution gate in hosted mode: a deliberate partial redirect the run already reports, such as a bundled copy left on the registry, is warned about and kept out of the in-run VEX. The engine's doc onunattributed_pins(hosted/engine.rs:1748-1753) states the same rule.- The Bun row for the same case (
redirect_bun_non_registry_entry_skipped, CLI_CONTRACT.md:1550): "the in-run VEX does not assume the patch, andvexattests nothing for thatname@version". - The npm rewriter already does this for a bundled copy (
redirect_npm_bundled_instance_skipped) and ahasShrinkwrapcopy (#753). Both insert the uuid intobundled_skipped_uuids"so the in-run--vexverifies instead of assuming the patch applied".
Actual. The tarball copy is warned about but not recorded, so the in-run VEX attests it. The directory copy is neither warned about nor recorded. In both shapes the in-run VEX contradicts what a standalone vex reports for the same tree.
OS × version
| OS | npm / Node | tarball shape | directory shape |
|---|---|---|---|
| Linux | 10.9.4 / 22.22.0 | repro (twice) | repro |
| Linux | 12.2.0 / 22.22.0 | repro | not run |
| Linux | 8.19.4 / 22.22.0 | n/a: npm 8 has no install-strategy, so it dedupes both specs onto the single file: copy and the scan correctly pins nothing |
n/a |
| macOS / Windows | — | not run (probe branches are blocked for this routine) | — |
First bad commit
Tarball shape: f3c6313a (#1008). On f3c6313a^ the same repro skips the patch with redirect_unattributable ("the rewrite would wire patch … but lockfile …"), writes no pin and no VEX statement, and leaves both copies unpatched. Since #1008 the pin is written and the in-run VEX attests it. The pnpm routine bisected #1469 to the same commit.
Directory shape: also attests on f3c6313a^. It isn't a regression.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:1502-1515: thenon_registrybranch warnsredirect_npm_non_registry_entry_skippedbut never insertsdep.patch_uuidintoresult.bundled_skipped_uuids. The bundled branch (:1470) and thehasShrinkwrapbranch (:1488) both do.crates/socket-patch-core/src/patch/redirect/mod.rs:1451: in the directory shape the copy is alinkentry with a separate target package entry (lpdir,version: 1.3.0). It isn't flagged at all, so no warning and no uuid record.crates/socket-patch-core/src/hosted/engine.rs:1896-1904: since #1008,unattributed_pinslets the shadowed ref through as a pin, and the in-run VEX then relies only onbundled_skipped_uuids.
No probe-run links: macOS and Windows probe branches are currently blocked for this routine.
- 主要言語
- Rust
- スター
- 8
- フォーク
- 1
- 平均マージ
- 18時間 19分
- マージ済み PR(30日)
- 504
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
SocketDev/socket-patch のほかの issue
-
agent:triaged bug bughunt pm:npm priority:p3 ux
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
SocketDev/socket-patch#1072 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
agent:triaged bug bughunt pm:yarn-berry priority:p1
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
SocketDev/socket-patch#783 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Vendored cargo warns cargo_multi_version_old_cargo ("declares no rust-version") when the root package inherits rust-version from [workspace.package]対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンagent:triaged bug bughunt pm:cargo priority:p2
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
SocketDev/socket-patch#651 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
Hosted gem scan ignores a Gemfile `lockfile "custom.lock"` DSL call (Bundler 4), so it pins the unused Gemfile.lock, leaves the real lock untouched, and every frozen install fails対応中かも @mikolalysenko が今日担当しました。 オープンagent:claimed agent:triaged bug bughunt pm:bundler priority:p1
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
SocketDev/socket-patch#1477 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
Hosted gem `rollback` / `remove` still strip the DEPENDENCIES `!` when the user's `source … do` block holds an assignment-form `v = if/case/begin … end`, so frozen installs fail on Bundler 4.0.19+対応中かも @mikolalysenko が今日担当しました。 オープンagent:claimed agent:triaged bug bughunt pm:bundler priority:p1
難易度 4/5 3〜5日 初心者へのやさしさ 25/100
SocketDev/socket-patch#1476 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
SocketDev/socket-patch の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
cmlarsen/writemorebetter#158 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
-
state:triage-needed
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
scanaislop/aislop#482 ·
メンテナーはふだん 1 日以内に返信
-
docs(skills): issue-first should reference an existing Linear ticket instead of filing a duplicateオープンdocumentation
難易度 1/5 1〜3時間 初心者へのやさしさ 74/100
メンテナーはふだん 1 日以内に返信