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)
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
[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.
- Ngôn ngữ chính
- Rust
- Star
- 8
- Fork
- 1
- Merge trung bình
- 16 giờ 35 phút
- Pull request đã merge (30 ngày)
- 574
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của SocketDev/socket-patch
-
agent:triaged bug bughunt pm:bundler priority:p1
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
SocketDev/socket-patch#1573 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
agent:triaged bug bughunt pm:poetry priority:p1
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 80/100
SocketDev/socket-patch#1547 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
agent:triaged bug bughunt pm:npm priority:p3 ux
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
SocketDev/socket-patch#1072 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
agent:triaged bug bughunt pm:yarn-berry priority:p1
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
SocketDev/socket-patch#783 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Vendored cargo warns cargo_multi_version_old_cargo ("declares no rust-version") when the root package inherits rust-version from [workspace.package]Có thể đã có người làm @mikolalysenko đã nhận 3 ngày trước. Đang mởagent:triaged bug bughunt pm:cargo priority:p2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
SocketDev/socket-patch#651 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của SocketDev/socket-patch
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
ActivityWatch/aw-notify-rs#47 · 1 bình luận ·
-
`inverse()` of a controlled `CUGate` drops the phase parameter γCó thể đã có người làm @rupasri118 đã nhận hôm nay. Đang mởbug mod: circuit
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
Qiskit/qiskit#17223 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 2 ngày
-
rustix::termios::Termios::OutputModes does not expost some const e.g. OLCUC for musl and macOSĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
bytecodealliance/rustix#1706 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 11 ngày
-
`LazyFrameRemote` and `ExecuteRemote` sinks do not accept `Path`, like `LazyFrame` sinks do.Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100