Spaces:
Running
Security model
This public Space accepts untrusted images, sends normalized image content to a configured external LLM provider, and renders generated procedural code in a visitor's browser. The controls and remaining limitations are explicit below.
Upload boundary
app/image_guard.py and app/main.py enforce:
- A 10 MiB default byte cap while reading, before image decode.
- Pillow content sniffing; filename, extension, and declared MIME type are not treated as proof of format.
- PNG, JPEG, WebP, GIF (first frame), and BMP only. SVG is rejected because it is scriptable XML.
- Pillow decompression-bomb protection, a 40 MP default pixel cap, and an 8192 px longest-side decode cap.
- EXIF orientation normalization followed by RGB PNG re-encoding, which strips original metadata. The normalized image is downscaled to at most 1024 px before forge/LLM processing.
- Server-generated job IDs and artifact names. The artifact route uses an explicit filename allowlist and verifies that the resolved file remains directly inside its job directory.
- Community publication is opt-out and disclosed before submission. It copies only the normalized reference and the generated artifact allowlist; original filenames and image metadata are not retained.
The normalized image is sent to the operator-configured LLM endpoint. Users must treat that provider as a data processor; this Space does not make a local model privacy claim.
Generated code and browser isolation
- The LLM authors JSON spec values, not executable TypeScript. A deterministic vendored generator emits code and JSON-escapes spec-derived literals.
- The hosted compiler accepts only its implemented primitive allowlist and rejects parent cycles. Generation fails if a TODO fallback is detected; an unsupported primitive is never silently represented as a placeholder box.
- The interactive viewer runs in
<iframe sandbox="allow-scripts">withoutallow-same-origin. It has an opaque origin and cannot access parent cookies, storage, or same-origin DOM. Parent/iframe communication ispostMessage; the parent validatesevent.source. - Normal application responses use a restrictive policy including
default-src 'self',script-src 'self' blob:,object-src 'none', andbase-uri 'none'./static/viewer.htmlreceives a separate explicit CSP permitting only its required inline bootstrap and blob module import. Its frame-ancestor list permits this Space itself and the canonicalhttps://huggingface.coembedding page, but no arbitrary third-party origin. - Generated artifacts are served as files. Standalone HTML base64-embeds the
bundle, creates a blob URL, and accesses module exports through
await import(url)rather than assuming export aliases are local bindings.
Browser sandboxing limits access to the Space origin; it is not a proof that arbitrary generated code is harmless in every browser implementation. Keep the sandbox and CSP in place when embedding or modifying the viewer.
Server-side process boundary
Forge, esbuild, and the isolated gallery publisher run with:
- list-form argv and
shell=False; - per-call timeouts and bounded captured output;
- a scrubbed child environment that omits
LLM_*andANTHROPIC_*variables; - server-controlled files inside a per-job temporary directory.
The strict-validated original spec remains locked and unreviewed. A deep-copied
hosted-unreviewed-preview manifest is used only to compile all supported
components for inspection. It carries no reviewer, screenshot, comparison
image, score, or upstream continue decision.
Persistent community gallery
- The private Hugging Face Bucket is mounted read/write at
/data; the app serves published entries through strict gallery routes rather than exposing the Bucket directly. - An opted-out job and every failed job stay in ephemeral job storage and are never published.
- Publication copies to a staging directory and makes the completed item visible only after its metadata manifest is atomically committed.
- Bucket copying runs in a scrubbed child process with a separate bounded deadline. Cancellation terminates and reaps that process, so a stalled mount cannot hold a conversion slot indefinitely.
- Gallery IDs use the same full-entropy server-generated format as jobs. Listing ignores malformed/incomplete entries. Artifact routes enforce an explicit allowlist plus direct-parent containment.
- Gallery entries intentionally persist across app restarts. Users should not upload images they are unwilling to publish while the share toggle is on.
Secrets and deployment
- Runtime credentials are read from environment variables at startup and are
never logged or returned by
/api/config. - Hugging Face injects Space Secrets at runtime; no credential is a Docker build argument or image layer.
scripts/deploy_space.pyuses the authenticated localhfCLI store, stages a deterministic file allowlist, and performs one upload from that sanitized directory.- Environment-derived secret values are written only to a temporary mode-0600
secrets file, passed via
hf spaces secrets add --secrets-file, and removed in afinallyblock. Logs and process arguments contain secret names only.
Abuse and resource control
- The default per-client limit is 10 accepted jobs per hour with
Retry-After. The in-memory client map has an LRU size bound. - Client identity comes from the ASGI socket peer as resolved by the trusted
Uvicorn proxy configuration. Application code does not reinterpret a raw,
caller-controlled
X-Forwarded-Forheader. - Two jobs run concurrently by default and queued plus running jobs are capped
at eight. A full queue returns
queue_fullwithRetry-After; queued uploads therefore cannot pin unbounded memory. - The conversion deadline starts when a job is accepted, includes queue time, and ends only after the browser bundle is complete. Queue, model, generator, bundler, and publisher waits emit elapsed-only feedback rather than invented percentages. Optional Bucket publication has its own shorter deadline.
- Provider retries apply only to transient statuses, not 400/401/403.
- Terminal jobs have a two-hour default TTL. The reaper does not delete a running job solely because it was created a long time ago.
All abuse controls are process-local. They reduce accidental/public-demo load; they are not a distributed quota system.
Container boundary
- The service runs as non-root UID 1000 under
tinionpython:3.12-slim-bookwormand binds to0.0.0.0:7860. - The final Docker stage copies only runtime dependencies,
app/, requiredforge/code, andLICENSE. .dockerignore, the runtime COPY allowlist, and Docker verification exclude VCS state, virtual environments, rollouts, caches, tests, scripts,upstream-src, and local verification artifacts from the final image.- Rebuild the image regularly to pick up base-image security updates.
Accepted limitations
- A single image does not reveal hidden geometry. Results are approximate and hidden sides are inferred, not observed or measured.
- A strict-valid spec is not visually approved. The hosted result is explicitly unreviewed; production use requires the upstream screenshot/comparison review loop.
- Opted-out/failed job artifacts live in ephemeral
/tmpand expire. Their hard-to-guess URLs are unlisted, not access-controlled. Published gallery entries persist in the mounted Bucket; this release does not provide a public deletion or moderation API. - Rate limiting and job state reset when the single process restarts.