Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de 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)

Abierto
#1,481 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
45/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
rust
Área
cli, security

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
Rust
Estrellas
8
Forks
1
Merge medio
18 h 19 min
PR fusionados (30 d)
504

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de SocketDev/socket-patch

Todos los issues de SocketDev/socket-patch

Issues similares

Más issues de Rust

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.