Embed + hosting — threat model
The embed snippet and the per-slug HTML hosting tier (spin-up-as-webapp) share an attack surface — they both let an outsider's bytes either run inside a host page on the public internet, or be served from a Zeq origin. This page collects the threats and the framework's mitigations. The canonical document is outputs/embed-and-host-security.md in the repo.
0. Trust boundaries
+-----------------------------------------------+
| untrusted: host-page DOM, snippet bytes, |
| embed key, third-party referrers |
+-----------------------------------------------+
|
+-----------v-----------+
| embed runtime | <-- CSP-safe, frozen API
+-----------+-----------+
| HTTPS, X-Zsm-Key
+-----------v-----------+
| /api/embed/:slug/... | <-- rate-limited, validated
+-----------+-----------+
|
+-----------v-----------+
| audit log + chain | <-- bytes hashed, never stored (free tier)
+-----------------------+
Rule: all user-facing bytes are untrusted; framework stores hashes/proofs/chain only. Paid hosting tier may store bytes under a separate contract.
1. Embed snippet — threats and mitigations
1.1 Host page tries to read framework data via window.zsm
window.zsm is Object.freezed and Object.defineProperty'd as writable:false, configurable:false. There is no read/get method — only one-way event / state / file / identify / ping / connection. The runtime never reads cookies, localStorage, sessionStorage, IndexedDB, or DOM text. It does not register any input listeners on host elements.
1.2 Malicious site uses someone else's slug+key to spam events
Public keys are visible in the snippet by design. Mitigations:
- Per-slug rate limit: 60 req/min per IP, 600 req/min per machine, 6,000 req/day per machine.
- Body-size cap: 4 KB per beacon, 64 KB max payload.
- Schema validation:
kindmust be one of{heartbeat, event, state, file, connection, identify, __unload}. Unknown kinds rejected. - Origin allow-list per machine (configurable in
/portal/embed). Beacons from off-list origins are accepted but flaggedcross_origin: trueand counted against a separate quota. - Public key prefix
zsm_pub_…is read-only-public; the owner'szsm_…write-back key NEVER ships.
1.3 XSS payload smuggled in the event data field
The runtime calls JSON.stringify on payload before transmission — there is no HTML rendering on the embed side. On the observer side, all rendered values pass through textContent (never innerHTML) per the project's CSP discipline. Any new render path that uses innerHTML on observer data is a regression.
1.4 Data exfiltration via beacon endpoint abuse
If the host page is XSS-compromised, the embed is downstream of that. The framework's record will faithfully capture whatever the host page tells it to record. The embed records exactly what the host page tells it to record. This is documented as a known limitation, not a vulnerability — the embed is a downstream sink.
For hardened deployments, a payload-field allowlist per machine can be configured in /portal/embed.
1.5 Timing side channel via heartbeat
Heartbeat cadence (5077 ms ≈ 6.5 Zeqonds) is fixed and identical across all installs. Phase carried in the payload is computed from Date.now() % 0.777 — no high-precision timer. No fingerprinting via beat jitter beyond ~10 ms accuracy.
1.6 postMessage-bridge abuse on the iframe form
The iframe-mode listener only relays whitelisted method names (event, state, file, identify, ping, connection). Any other zsm: key is dropped. The iframe never echoes a value back — there's no postMessage reply, so the parent cannot use the iframe as a confused-deputy oracle.
1.7 Clickjacking the iframe
The iframe page is served with Content-Security-Policy: frame-ancestors * (it's meant to be framed) but with display:none; width:0; height:0 recommended in the snippet — there is no UI to click. Even if a malicious host frames it visibly, there is no clickable surface.
1.8 CSP-strict host pages refuse to load embed
The script form requires script-src https://zeqapi.com in the host CSP. The iframe form requires frame-src https://zeqapi.com. The pixel form requires img-src https://zeqapi.com. All three documented in /portal/embed.
1.9 Supply-chain compromise of zeq.dev/embed.js
embed.jsserved withContent-Security-Policy: default-src 'none'; connect-src 'self'. The runtime can callfetchandsendBeacon, nothing else.- Subresource Integrity:
/portal/embedincludes a copy-paste variant withintegrity="sha384-…"andcrossorigin="anonymous". - Build pipeline pins
embed.jsSHA-256 inapps/zeq-dev/public/embed.lock.json. CI fails if served bytes don't match.
2. State-channel HTML hosting — threats and mitigations
The hosting tier lets a machine owner upload arbitrary HTML/JS/CSS that gets served at https://zeqapi.com/s/:slug/* (or a subdomain — see the open question below). External data (user backends, third-party APIs) stays external. Framework stores only hashes/proofs/chain.
2.1 Hosted site runs JS that calls Zeq APIs as the machine owner
Hosted bytes are served from a separate origin — not zeq.dev. Production: https://*.zeqsite.app (or https://:slug.zeqsite.app if subdomain-per-slug is feasible). Cookies/CORS for zeq.dev are not in scope. The hosted page CANNOT read or send zeq.dev cookies.
2.2 Hosted site exfiltrates data via beacon to attacker's server
Strict CSP applied at serve time (response header is framework-controlled, NOT user-supplied):
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'none';
sandbox allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox;
The site cannot fetch external origins. If the machine owner needs external API calls, they go through a proxy under their own state machine — /api/chain/:slug/proxy?url=... — auth-gated, rate-limited, with an owner-allowlisted URL pattern.
2.3 Hosted site frames itself or others to clickjack
frame-ancestors 'none' (above) plus X-Frame-Options: DENY. The hosted site cannot itself be framed. Per-slug exception requestable in /portal/host.
2.4 File upload of malicious binary served as application/javascript
MIME is forced server-side from a fixed map of file extensions. Owner cannot override. Unknown extensions get application/octet-stream + Content-Disposition: attachment.
2.5 Slug squatting / takeover
Slug ownership is bound to the spinning Zeq ID at create time and recorded on the entangled state. Slug recycling requires a 7-day TTL after explicit delete. The serve handler joins on state_machines by slug → owner_zid; if owner_zid was rotated, all hosted bytes 404 until re-claimed.
2.6 Hash collision / chain-tampering with hosted bytes
Every served byte stream is committed to chain as sha256(serve_bytes) || serve_path || zeqond_number. Free tier stores only the digest; paid tier stores bytes alongside (separate billing path). Reads include X-Zsm-Content-Sha256 response header so external verifiers can independently re-hash.
2.7 Cross-slug pollution via shared static assets
Each slug gets an isolated namespace /s/:slug/; assets cannot reference /s/other-slug/. Server-side path normalization rejects .., absolute paths, and any path that resolves above the machine root.
2.8 DDoS via cheap-to-host content
Per-slug bandwidth quota: free tier 1 GB/month egress, hard cutoff. Per-slug request quota: 10K serves/day free tier. Quotas live in tallyEconomy.ts alongside compute credits.
2.9 SEO / phishing piggyback on zeqsite.app reputation
zeqsite.app ships a robots.txt and meta-robots default of noindex, nofollow for free tier. Owner can request indexing via /portal/host/seo after domain-verification check. Suspected-phishing reports go to an internal review queue; admin can disable a machine (404 + chain row of disable).
3. Where embed and host meet
A common pattern: a machine owner hosts an HTML page at /s/:slug/landing AND drops embed.js on it. The hosted page is on zeqsite.app; the embed beacons to zeq.dev/api/embed/:slug/ingest. Cross-origin from page → embed endpoint is enabled by:
Access-Control-Allow-Origin: *(the public key already gates abuse).Access-Control-Allow-Methods: POST, OPTIONS.Access-Control-Allow-Headers: Content-Type, X-Zsm-Key.- No
Access-Control-Allow-Credentials— embed is cookie-less by design.
This is the only route on zeq.dev that allows wildcard CORS. Every other route stays origin-locked.
4. Audit hooks
Every numbered threat above maps to a unit test in shared/api-core/src/__tests__/:
| Threat | Test file (proposed) |
|---|---|
| 1.1 frozen API | embedFrozen.test.ts (jsdom) |
| 1.2 rate limit | embedRateLimit.test.ts |
| 1.3 schema | embedSchema.test.ts |
| 1.6 postMessage whitelist | embedPostMessage.test.ts |
| 1.9 SRI / lock | extends chrome-ext-paths.test.ts to cover embed.lock.json |
| 2.1 origin separation | hostOrigin.test.ts |
| 2.2 CSP header | hostCSP.test.ts |
| 2.4 MIME forcing | hostMime.test.ts |
| 2.5 slug ownership | hostOwnership.test.ts |
| 2.6 hash commitment | hostHash.test.ts |
| 2.7 path traversal | hostTraversal.test.ts |
| 2.8 quotas | hostQuotas.test.ts |
5. Open questions for project-lead sign-off
- Hosted-content origin —
zeqsite.app(separate domain, requires DNS) vs.*.zeqsite.zeq.dev(single cert wildcard, no extra domain). Recommendation: separate domain to fully isolate cookie scope. - Paid hosting tier byte storage — store in S3-compatible bucket or in Postgres
bytea? Recommendation: bucket; chain stores only hash + bucket pointer. - Embed key rotation cadence — manual only, or auto-rotate every N days? Recommendation: manual; auto-rotate is too aggressive for snippets pasted across many sites.
- Embed pixel/IoT auth — public key in querystring is logged in CDN access logs. Acceptable risk for
pub_keys (read-only-public anyway). Confirm with owner.