Summary
The Flask API under api/ had no authentication on any route, including
/v1/video/rendering, which writes user-submitted code verbatim to a
.py file and executes it via the manim CLI. Combined with a Dockerfile
that ran the container as root, this allowed any unauthenticated network
caller to execute arbitrary code as root inside the container. Two related
issues were found alongside it: an SSRF in the video-export endpoint and a
path-traversal issue via an unsanitized file_class field.
Details
1. Unauthenticated remote code execution (Critical, CWE-94)
POST /v1/video/rendering wrote the code field from the request body
directly into a .py file and ran it via manim, which imports the file
as a module, meaning module-level statements execute immediately without
needing a valid Scene class. No authentication, sandboxing, or isolation
gated this route. The shipped Dockerfile had no USER directive, so the
process ran as root, confirmed via a proof-of-concept that wrote
uid=0(root) gid=0(root) to a file inside the container.
2. Server-side request forgery (High, CWE-918)
POST /v1/video/exporting called requests.get(video_url) on a fully
user-controlled URL (scenes[].videoUrl) with no validation, allowing the
server to be made to issue requests to internal services or cloud
metadata endpoints (e.g. 169.254.169.254).
3. Path traversal via file_class (High, CWE-22 / CWE-200)
file_class in POST /v1/video/rendering was used unsanitized to locate
the rendered output file, which was then moved into the public web
directory. A value such as ../../../../tmp/secret could be used to move
an arbitrary .mp4-suffixed file into the public directory and serve it.
In practice, reaching this path also required control over code (to get
manim to exit successfully against the crafted file_class), so it was
primarily a compounding factor on top of (1) rather than an independently
exploitable bug.
Fixes
- #85 validates and restricts outbound URLs in the export endpoint,
blocking private/loopback/link-local/reserved addresses and disabling
redirects (fixes SSRF).
- #86 requires
file_class to be a bare identifier, eliminating the
traversal primitive (fixes path traversal).
- #87 adds an
API_KEY-based authentication requirement (X-API-Key
header) enforced on all routes except health/root/static assets (closes
the fully unauthenticated gap).
- #88 runs the container as a dedicated non-root user, limiting the impact
of any future code-execution bug in the render path.
Executing user-submitted code is inherent to this project's rendering
feature; authentication and non-root execution reduce risk but do not
replace full sandboxing. Deployers who accept code or prompts from
untrusted users should still isolate render workers, per the Security
section of docs/cloud-deployment.md.
Credits
Reported privately by Saher Boujemline (@KezoSec),
who reproduced all three issues locally and coordinated disclosure with
advance notice before any public write-up. Thank you for the responsible
disclosure.
Summary
The Flask API under
api/had no authentication on any route, including/v1/video/rendering, which writes user-submittedcodeverbatim to a.pyfile and executes it via themanimCLI. Combined with a Dockerfilethat ran the container as root, this allowed any unauthenticated network
caller to execute arbitrary code as root inside the container. Two related
issues were found alongside it: an SSRF in the video-export endpoint and a
path-traversal issue via an unsanitized
file_classfield.Details
1. Unauthenticated remote code execution (Critical, CWE-94)
POST /v1/video/renderingwrote thecodefield from the request bodydirectly into a
.pyfile and ran it viamanim, which imports the fileas a module, meaning module-level statements execute immediately without
needing a valid
Sceneclass. No authentication, sandboxing, or isolationgated this route. The shipped
Dockerfilehad noUSERdirective, so theprocess ran as root, confirmed via a proof-of-concept that wrote
uid=0(root) gid=0(root)to a file inside the container.2. Server-side request forgery (High, CWE-918)
POST /v1/video/exportingcalledrequests.get(video_url)on a fullyuser-controlled URL (
scenes[].videoUrl) with no validation, allowing theserver to be made to issue requests to internal services or cloud
metadata endpoints (e.g.
169.254.169.254).3. Path traversal via
file_class(High, CWE-22 / CWE-200)file_classinPOST /v1/video/renderingwas used unsanitized to locatethe rendered output file, which was then moved into the public web
directory. A value such as
../../../../tmp/secretcould be used to movean arbitrary
.mp4-suffixed file into the public directory and serve it.In practice, reaching this path also required control over
code(to getmanimto exit successfully against the craftedfile_class), so it wasprimarily a compounding factor on top of (1) rather than an independently
exploitable bug.
Fixes
blocking private/loopback/link-local/reserved addresses and disabling
redirects (fixes SSRF).
file_classto be a bare identifier, eliminating thetraversal primitive (fixes path traversal).
API_KEY-based authentication requirement (X-API-Keyheader) enforced on all routes except health/root/static assets (closes
the fully unauthenticated gap).
of any future code-execution bug in the render path.
Executing user-submitted code is inherent to this project's rendering
feature; authentication and non-root execution reduce risk but do not
replace full sandboxing. Deployers who accept code or prompts from
untrusted users should still isolate render workers, per the Security
section of
docs/cloud-deployment.md.Credits
Reported privately by Saher Boujemline (@KezoSec),
who reproduced all three issues locally and coordinated disclosure with
advance notice before any public write-up. Thank you for the responsible
disclosure.