The backend is a local developer tool that reads episode files from a
configured directory and serves them over HTTP to the local frontend. It is not
intended to be exposed to the public internet. Even so, the most realistic risk
— a crafted episode_id reading arbitrary files — is defended against.
episode_id arrives from the network and is untrusted. Two layers:
- Charset allow-list (
storage/paths.py):^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$and an explicit".." not in idcheck. This rejects/,.., encoded traversal, and absolute paths before any filesystem access. The HTTP layer (server/security.py) maps a rejected id to 400. - Containment re-check:
safe_episode_dirresolvesroot/episode_idand verifies the result is still inside the resolved episodes root (guards against symlink trickery).
Covered by tests/unit/test_storage_validation.py::test_path_traversal_rejected
and tests/integration/test_api_server.py::test_path_traversal_rejected.
- Read-only API. All routes are
GET; CORS allows onlyGETfrom localhost dev origins (server/app.py). - No information leak on errors. Unknown ids return a generic 404; validation errors describe the data problem, not host filesystem layout.
- No secrets / no private paths in the repo. Committed sample episodes
(
sample_data/) contain only demo data from public Isaac Lab tasks; generated Cartpole data is git-ignored and reproducible from the deterministic fake generator. The Isaac Lab path is configurable (ISAACLABmake var), not baked into committed code.
Put it behind a reverse proxy with auth/TLS and bind the backend to 127.0.0.1
(the default). Do not point --episodes-dir at a directory that contains
anything but episodes.