img2threejs / docs /SECURITY.md
Mike0021's picture
Bound long-running stages and expand progress feedback
bf1fb5f verified
|
Raw
History Blame Contribute Delete
7.7 kB

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"> without allow-same-origin. It has an opaque origin and cannot access parent cookies, storage, or same-origin DOM. Parent/iframe communication is postMessage; the parent validates event.source.
  • Normal application responses use a restrictive policy including default-src 'self', script-src 'self' blob:, object-src 'none', and base-uri 'none'. /static/viewer.html receives a separate explicit CSP permitting only its required inline bootstrap and blob module import. Its frame-ancestor list permits this Space itself and the canonical https://huggingface.co embedding 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_* and ANTHROPIC_* 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.py uses the authenticated local hf CLI 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 a finally block. 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-For header.
  • Two jobs run concurrently by default and queued plus running jobs are capped at eight. A full queue returns queue_full with Retry-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 tini on python:3.12-slim-bookworm and binds to 0.0.0.0:7860.
  • The final Docker stage copies only runtime dependencies, app/, required forge/ code, and LICENSE.
  • .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 /tmp and 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.