Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

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)

Đã đóng
#1,481 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

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
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
rust
Lĩnh vực
cli, security

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ả

bug bughunt pm:npm

[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 on unattributed_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, and vex attests nothing for that name@version".
  • The npm rewriter already does this for a bundled copy (redirect_npm_bundled_instance_skipped) and a hasShrinkwrap copy (#753). Both insert the uuid into bundled_skipped_uuids "so the in-run --vex verifies 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: the non_registry branch warns redirect_npm_non_registry_entry_skipped but never inserts dep.patch_uuid into result.bundled_skipped_uuids. The bundled branch (:1470) and the hasShrinkwrap branch (:1488) both do.
  • crates/socket-patch-core/src/patch/redirect/mod.rs:1451: in the directory shape the copy is a link entry 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_pins lets the shadowed ref through as a pin, and the in-run VEX then relies only on bundled_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

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của SocketDev/socket-patch

Tất cả issue của SocketDev/socket-patch

Issue tương tự

Thêm issue về Rust

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.