{"id":"GHSA-rpj2-4hq8-938g","aliases":[],"url":"https://o3.security/vulnerability/GHSA-rpj2-4hq8-938g","summary":"VCR.py: Arbitrary code execution via unsafe YAML deserialization of cassette files","details":"### Summary\n\nvcrpy deserializes YAML cassette files with PyYAML's object-constructing loader (`yaml.CLoader` / `yaml.Loader`) instead of the safe loader (`yaml.CSafeLoader` / `yaml.SafeLoader`). A cassette containing a `!!python/object/apply:` (or similar) tag therefore executes arbitrary Python code the moment the cassette is loaded — including through the normal `VCR().use_cassette()` path, before any HTTP interaction is replayed.\n\nThis is **not** limited to environments lacking the libYAML C extension. `CLoader` uses the C parser but PyYAML's full Python *constructor*, so Python\nobject tags execute under `CLoader` exactly as under the pure-Python `Loader`. Confirmed against vcrpy 8.1.1 + PyYAML 6.0.3 with `CLoader` active.\n\n### Affected component\n\n- `vcr/serializers/yamlserializer.py` — `deserialize()` → `yaml.load(cassette_string, Loader=Loader)` where `Loader` is `CLoader`/`Loader`. Reached on **every** cassette load.\n- `vcr/migration.py` (~line 107) — `yaml.load(preprocess_yaml(...), Loader=Loader)`. A second sink reached when the migration tool is run on a `.yaml` file. `preprocess_yaml()` only strips three known legacy tags, so other tags still execute.\n\nPresent in all releases inspected, 1.0.0 through 8.1.1.\n\n### Proof of concept\n\n```python\nimport vcr, requests\n\n# Attacker-supplied cassette. The payload sits in an ignored top-level key\n# so the rest of the cassette stays valid; it fires during load.\nopen(\"evil.yaml\", \"w\").write(\"\"\"interactions:\n- request:\n    body: null\n    headers: {Accept: ['*/*']}\n    method: GET\n    uri: http://example.com/\n  response:\n    body: {string: ok}\n    headers: {Content-Type: ['text/plain']}\n    status: {code: 200, message: OK}\n_x: !!python/object/apply:os.system ['touch /tmp/VCRPY_YAML_RCE']\nversion: 1\n\"\"\")\n\nwith vcr.use_cassette(\"evil.yaml\"):      # <-- /tmp/VCRPY_YAML_RCE created here\n    requests.get(\"http://example.com/\")\n```\n\nLoading the cassette creates `/tmp/VCRPY_YAML_RCE`, demonstrating arbitrary command execution. Any Python callable can be invoked this way.\n\n### Impact\n\nArbitrary code execution in the process that loads the cassette, with that process's full privileges. Realistic delivery paths:\n\n- A malicious cassette added in a pull request and loaded when CI runs the tests.\n- A poisoned shared test-fixture repository or cassette artifact store.\n- \"Updated recorded HTTP fixtures\" social-engineering.\n\nBecause cassettes are typically loaded by test suites in CI/CD and on developer machines, the exposed secrets are exactly the high-value ones in those environments: CI deployment credentials, cloud IAM roles, registry/publishing tokens, and source access.\n\n### Patch\n\nUse the safe loader in `vcr/serializers/yamlserializer.py`:\n\n```python\ntry:\n    from yaml import CDumper as Dumper\n    from yaml import CSafeLoader as Loader\nexcept ImportError:\n    from yaml import Dumper\n    from yaml import SafeLoader as Loader\n\ndef deserialize(cassette_string):\n    return yaml.load(cassette_string, Loader=Loader)\n```\n\nApply the same `SafeLoader` change in `vcr/migration.py`.\n\nThis is backwards compatible: vcrpy cassettes only contain standard YAML (scalars/lists/maps plus `!!binary`, all supported by `SafeLoader`/`CSafeLoader`), so existing cassettes load unchanged. vcrpy's `serialize.deserialize()` already catches `yaml.constructor.ConstructorError`, so a Python-tagged cassette now surfaces as the existing \"old cassette format\" `ValueError` instead of executing.\n\nRecommended hardening: add a regression test that loads a cassette containing `!!python/object/apply:os.system` and asserts a `ConstructorError`/`ValueError` and that no side effect occurs.","published":"2026-06-19T21:15:47Z","modified":"2026-06-19T21:30:10.214252001Z","cvss":{"score":7.8,"severity":"HIGH","vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"vcrpy","fixedVersion":"8.2.1"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/kevin1024/vcrpy/security/advisories/GHSA-rpj2-4hq8-938g"},{"type":"PACKAGE","url":"https://github.com/kevin1024/vcrpy"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-06-19T21:30:10.214252001Z"}}