Spaces:
Running
Running
File size: 7,697 Bytes
39ff632 37e3d5a 39ff632 37e3d5a 39ff632 bf1fb5f 39ff632 37e3d5a bf1fb5f 37e3d5a 39ff632 bf1fb5f 39ff632 37e3d5a 39ff632 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | # 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.
|