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.