yaml-practice
b01lersc 2026
Task: a Starlette YAML validator backed by Prefect accepted uploaded .yml files and only checked the filename suffix. Solution: use path traversal to overwrite Prefect's packaged logging.yml, then trigger dictConfig-based command execution during /validate and exfiltrate the flag.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
yaml-practice — b01lersc 2026
Overview
yaml-practice is a small Starlette application that uploads YAML files and validates them through a Prefect flow. The bug is not in YAML parsing itself, but in how the upload endpoint handles filenames: it only checks that the suffix is .yml, then writes the file to UPLOAD_DIR / safe_name without normalizing traversal sequences.
That gives an arbitrary file overwrite primitive. The winning target is Prefect's packaged logging configuration at ../.venv/lib/python3.14/site-packages/prefect/logging/logging.yml. After replacing that file with a malicious logging config, a call to /validate causes Prefect to load it with yaml.safe_load(...) and pass it into logging.config.dictConfig(...), which can instantiate attacker-controlled callables.
An earlier knowledge-base writeup, Resourcehub Core, suggested looking for arbitrary file overwrite through upload handling, but this challenge is a different exploit chain: Python + Prefect + logging configuration abuse rather than Node/route overwrite.
Source Analysis
The important application logic is short:
def check_filename(filename: str) -> str: if Path(filename).suffix.lower() != ALLOWED_EXTENSION: raise ValueError("Only .yml files are allowed.") return filename async def upload_yaml(request: Request) -> JSONResponse: ... safe_name = check_filename(filename) ... file_path = UPLOAD_DIR / safe_name file_path.write_text(decoded_contents) async def validate_yaml(request: Request) -> JSONResponse: ... result = validate_yaml_flow(file_path)
The critical detail is that check_filename() validates only the suffix:
Path(filename).suffix.lower() == ".yml"- no
resolve() - no
..rejection - no basename enforcement on upload
So a filename like:
../.venv/lib/python3.14/site-packages/prefect/logging/logging.yml
passes validation because its suffix is still .yml, and the server writes outside uploads/.
The /validate route later calls validate_yaml_flow(file_path). That enters Prefect internals, and Prefect loads its logging configuration from prefect/logging/logging.yml. That file is parsed with yaml.safe_load and then fed to logging.config.dictConfig.
Vulnerability
This is an exploit chain built from two behaviors:
...