Skip to content

Commit 5bb1cb9

Browse files
committed
bughunt pip probe: -g with pip --user
1 parent 6e7ef74 commit 5bb1cb9

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

‎.github/workflows/bughunt-pip.yml‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
name: bughunt pip probe (global --user)
2+
on:
3+
push:
4+
branches: ['bughunt/pip/**']
5+
permissions:
6+
contents: read
7+
jobs:
8+
probe:
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
os: [ubuntu-latest, macos-latest, windows-latest]
13+
include:
14+
- python: '3.8'
15+
pipv: '20.3.4'
16+
- python: '3.13'
17+
pipv: ''
18+
python: ['3.8', '3.13']
19+
runs-on: ${{ matrix.os }}
20+
timeout-minutes: 45
21+
steps:
22+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
23+
with:
24+
persist-credentials: false
25+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
26+
with:
27+
python-version: ${{ matrix.python }}
28+
- name: Install Rust
29+
run: rustup show
30+
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
31+
with:
32+
key: bughunt-pip-g
33+
save-if: false
34+
- name: Build the CLI
35+
shell: bash
36+
run: cargo build --locked -p socket-patch-cli
37+
- name: Write probe
38+
shell: bash
39+
run: |
40+
cat > "$RUNNER_TEMP/probe_g.py" <<'PYEOF'
41+
import base64, csv, hashlib, http.server, io, json, os, subprocess, sys, tempfile, threading, urllib.request, zipfile
42+
CLI = os.environ["SP_CLI"]; WIN = os.name == "nt"
43+
UUID = "4d5e6f70-8192-4a1b-8c2d-0123456789ab"; TOKEN = "11111111-1111-4111-8111-111111111111"
44+
work = tempfile.mkdtemp(prefix="bh pip ")
45+
def run(cmd, cwd=None, env=None, check=False):
46+
p = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True)
47+
if check and p.returncode: print(p.stdout, p.stderr); raise SystemExit(f"failed: {cmd}")
48+
return p
49+
# patched wheel
50+
up = os.path.join(work, "six-1.16.0-py2.py3-none-any.whl")
51+
urllib.request.urlretrieve("https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", up)
52+
zin = zipfile.ZipFile(up); buf = io.BytesIO(); zout = zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED)
53+
ORIG = zin.read("six.py"); patched_six = ORIG + b"\nSOCKET_PATCHED = 1\n"
54+
def gsha(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest()
55+
for item in zin.infolist():
56+
data = zin.read(item.filename)
57+
if item.filename == "six.py": data = patched_six
58+
if item.filename.endswith("RECORD"):
59+
rows = []
60+
for row in csv.reader(io.StringIO(data.decode())):
61+
if row and row[0] == "six.py":
62+
row = ["six.py", "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(patched_six).digest()).rstrip(b"=").decode(), str(len(patched_six))]
63+
rows.append(row)
64+
s = io.StringIO(); csv.writer(s, lineterminator="\n").writerows(rows); data = s.getvalue().encode()
65+
zout.writestr(item, data)
66+
zout.close(); WHL = buf.getvalue(); SHA = hashlib.sha256(WHL).hexdigest()
67+
class H(http.server.BaseHTTPRequestHandler):
68+
def log_message(self, *a): pass
69+
def send(self, code, body, ct="application/json"):
70+
b = body if isinstance(body, bytes) else json.dumps(body).encode()
71+
self.send_response(code); self.send_header("Content-Type", ct); self.send_header("Content-Length", str(len(b))); self.end_headers()
72+
if self.command != "HEAD": self.wfile.write(b)
73+
def do_HEAD(self): self.do_GET()
74+
def do_GET(self):
75+
if self.path.endswith(".whl"): return self.send(200, WHL, "application/octet-stream")
76+
if "/patches/blob/" in self.path or "/patch/blob/" in self.path:
77+
h = self.path.rsplit("/", 1)[1]
78+
for b in (ORIG, patched_six):
79+
if gsha(b) == h: return self.send(200, b, "application/octet-stream")
80+
return self.send(404, {"error": "nf"})
81+
if "/patches/view/" in self.path:
82+
return self.send(200, {"uuid": UUID, "purl": "pkg:pypi/six@1.16.0", "publishedAt": "2024-01-01T00:00:00Z", "files": {"six.py": {"beforeHash": gsha(ORIG), "afterHash": gsha(patched_six), "blobContent": base64.b64encode(patched_six).decode()}}, "vulnerabilities": {"GHSA-xxxx-yyyy-zzzz": {"cves": ["CVE-2024-88888"], "summary": "s", "severity": "high", "description": "d"}}, "description": "d", "license": "MIT", "tier": "free"})
83+
if "/patches/by-package/" in self.path:
84+
hit = "six%401.16.0" in self.path
85+
return self.send(200, {"patches": [{"uuid": UUID, "purl": "pkg:pypi/six@1.16.0", "publishedAt": "2024-01-01T00:00:00Z", "description": "d", "license": "MIT", "tier": "free", "vulnerabilities": {"GHSA-xxxx-yyyy-zzzz": {"cves": ["CVE-2024-88888"], "summary": "s", "severity": "high", "description": "d"}}}] if hit else [], "canAccessPaidPatches": True})
86+
self.send(404, {"error": "nf"})
87+
def do_POST(self):
88+
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
89+
if self.path.endswith("/patches/package"):
90+
url = f"http://127.0.0.1:{PORT}/patch/pypi/six/1.16.0/{TOKEN}/{UUID}/six-1.16.0-py2.py3-none-any.whl"
91+
return self.send(200, {"results": {UUID: {"status": "granted", "url": url, "purl": "pkg:pypi/six@1.16.0", "artifacts": [{"kind": "tarball", "url": url, "integrity": {"sha256": SHA}}], "registryOverride": None}}})
92+
if self.path.endswith("/patches/batch"):
93+
comps = [c["purl"] for c in json.loads(body or b"{}").get("components", [])]
94+
pk = [{"purl": "pkg:pypi/six@1.16.0", "patches": [{"uuid": UUID, "purl": "pkg:pypi/six@1.16.0", "tier": "free", "cveIds": ["CVE-2024-88888"], "ghsaIds": ["GHSA-xxxx-yyyy-zzzz"], "severity": "high", "title": "t"}]}] if "pkg:pypi/six@1.16.0" in comps else []
95+
return self.send(200, {"packages": pk, "canAccessPaidPatches": True})
96+
self.send(200, {})
97+
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H); PORT = srv.server_address[1]
98+
threading.Thread(target=srv.serve_forever, daemon=True).start()
99+
100+
import shutil, site
101+
env = {k: v for k, v in os.environ.items() if not k.startswith("SOCKET_") and k != "VIRTUAL_ENV"}
102+
env["SOCKET_API_TOKEN"] = "sktsec_" + "a" * 44 + "_api"
103+
API = ["--api-url", f"http://127.0.0.1:{PORT}", "--org", "test-org"]
104+
py = sys.executable
105+
if os.environ.get("PIPV"): run([py, "-m", "pip", "install", "-q", "--disable-pip-version-check", "pip==" + os.environ["PIPV"]], check=True)
106+
print("pip", run([py, "-m", "pip", "--version"]).stdout.strip())
107+
r = run([py, "-m", "pip", "install", "--disable-pip-version-check", "--user", "--force-reinstall", "six==1.16.0"] + os.environ.get("PIP_EXTRA", "").split())
108+
print("user install rc", r.returncode, r.stderr[-400:])
109+
usersite = run([py, "-c", "import site;print(site.getusersitepackages())"]).stdout.strip()
110+
six_path = os.path.join(usersite, "six.py"); print("USER_SITE", usersite, os.path.exists(six_path))
111+
before = open(six_path, "rb").read() if os.path.exists(six_path) else b""
112+
proj = os.path.join(work, "outside"); os.makedirs(proj)
113+
def cli(*args):
114+
p = run([CLI, *args, "--json", "--cwd", proj] + API, cwd=proj, env=env)
115+
try: return p.returncode, json.loads(p.stdout)
116+
except Exception: return p.returncode, {"raw": p.stdout[-600:], "err": p.stderr[-600:]}
117+
def oracle():
118+
o = run([py, "-c", "import six;print('PATCHED' if getattr(six,'SOCKET_PATCHED',0) else 'UNPATCHED', six.__file__)"], cwd=proj)
119+
return o.stdout.strip() or o.stderr.strip()[-200:]
120+
rc, j = cli("scan", "-g")
121+
purls = [p.get("purl") for p in j.get("packages", [])] if isinstance(j.get("packages"), list) else j
122+
print(f"CELL scan-g-report rc={rc} scanned={j.get('scannedPackages')} six_found={'pkg:pypi/six@1.16.0' in str(purls)} status={j.get('status')} file_unchanged={open(six_path,'rb').read()==before}")
123+
rc, j = cli("scan", "-g", "--mode", "hosted", "--yes")
124+
print(f"CELL scan-g-hosted-refused rc={rc} status={j.get('status')} err={str(j.get('error') or j.get('raw'))[:200]}")
125+
rc, j = cli("get", UUID, "-g", "--yes")
126+
print(f"CELL get-g rc={rc} status={j.get('status')} oracle={oracle()} detail={json.dumps(j)[:400]}")
127+
rc, j = cli("rollback", "-g", "--yes")
128+
print(f"CELL rollback-g rc={rc} status={j.get('status')} byte_exact={open(six_path,'rb').read()==before} oracle={oracle()}")
129+
print("ROLLBACK_JSON", json.dumps(j)[:1500])
130+
PYEOF
131+
- name: Run probe
132+
shell: bash
133+
env:
134+
PIPV: ${{ matrix.pipv }}
135+
SOCKET_NO_CONFIG: '1'
136+
run: |
137+
if [ "$RUNNER_OS" = Windows ]; then export SP_CLI="$GITHUB_WORKSPACE/target/debug/socket-patch.exe"; else export SP_CLI="$GITHUB_WORKSPACE/target/debug/socket-patch"; fi
138+
python "$RUNNER_TEMP/probe_g.py"

0 commit comments

Comments
 (0)