feat(13-09): enforce the /aiui/-scoped CSP sandbox boundary (AIUI-04)

Adds a Content-Security-Policy header to both nginx `location /aiui/`
blocks whose connect-src is scoped to the AIUI path prefix, so AIUI's
own JavaScript is browser-prevented from issuing a same-origin fetch
to /rpc/v1 with the ambient session cookie. Explicitly rejects the
`sandbox` iframe attribute (allow-scripts + allow-same-origin is the
known escape; dropping allow-same-origin breaks AIUI's storage and
its origin-checked bridge) and records why in both the nginx comment
and a new comment above the Chat.vue iframe. Adds
referrerpolicy="no-referrer" to the iframe so a media URL or page path
never leaks upstream via Referer.

Also adds an explicit `location /aiui/api/openrouter/ { return 404; }`
to both server blocks, closing 13-02's Task 3 checkpoint finding
(operator-accepted deviation 2026-08-03): the relay was already
structurally gone but the SPA catch-all served 200/405 instead of 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-04 03:11:36 -04:00
co-authored by Claude Opus 5
parent 5e3aab30a2
commit 6ac0ebbf0a
2 changed files with 54 additions and 1 deletions
@@ -35,9 +35,37 @@ server {
}
# AIUI SPA (Chat mode iframe) — SPA fallback for client-side routing
#
# /aiui/-scoped CSP (AIUI-04, D-19 unaffected — this is a build-time/
# runtime property, not a repository-location one): this header governs
# ONLY the document served from this location (it replaces, not adds to,
# the site-wide policy above — nginx add_header does not inherit from
# the previous level once the current level declares its own, same as
# the Cache-Control line below already does at this location). Its
# connect-src is scoped to the AIUI path prefix, so AIUI's own
# JavaScript is browser-prevented from issuing a same-origin fetch to
# /rpc/v1 with the ambient session cookie. This makes AIUI-04's
# "sandboxed by construction" an enforced boundary rather than the
# code-discipline convention the old proxy comment further down
# mistakenly implied. It does NOT split AIUI onto a different origin —
# DOM, storage, and cookies are still shared with the rest of the site;
# only what this policy polices (script/style/connect/etc. sources) is
# restricted. The residual risk (a browser that ignores or partially
# enforces CSP) is named, not silently assumed away, in 13-AI-SPEC.md
# §6 and mitigated by G-B3's rate limit on assistant.chat (13-12).
location /aiui/ {
try_files $uri $uri/ /aiui/index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' blob: data:; connect-src $scheme://$host:*/aiui/ blob: data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
}
# AIUI's own JS reaching a paid/relay path directly (openrouter was
# deleted outright in 13-02 — no proxy_pass to openrouter.ai survives
# anywhere in this config) must not silently 200 via the SPA catch-all
# below. Explicit here rather than bolted onto 13-02 after the fact
# (13-02's Task 3 checkpoint, operator-accepted 2026-08-03).
location /aiui/api/openrouter/ {
return 404;
}
# AIUI assets fallback — AIUI may reference /assets/ without /aiui/ prefix
@@ -956,10 +984,24 @@ server {
}
# AIUI SPA (Chat mode iframe) — SPA fallback for client-side routing
#
# /aiui/-scoped CSP — see the HTTP server block above for the full
# rationale (AIUI-04, D-19 unaffected). Both server blocks must carry
# this header — a change applied to only one leaves AIUI's JS able to
# reach /rpc/v1 with the ambient session cookie on whichever block
# actually serves the request, same class of gap as T-13-15.
location /aiui/ {
try_files $uri $uri/ /aiui/index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' blob: data:; connect-src $scheme://$host:*/aiui/ blob: data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
}
# AIUI's own JS reaching a paid/relay path directly must not silently
# 200 via the SPA catch-all below — see the HTTP server block above.
location /aiui/api/openrouter/ {
return 404;
}
# See the HTTP server block above for the full rationale: re-pointed to
# the session-gated Rust daemon (T-13-08/T-13-09), OpenRouter relay
# deleted outright (T-13-10). Both server blocks must carry this fix —
+12 -1
View File
@@ -30,7 +30,17 @@
</div>
</Transition>
<!-- AIUI iframe on mobile, leave room for close bar + tab bar at bottom -->
<!-- AIUI iframe on mobile, leave room for close bar + tab bar at bottom.
No `sandbox` attribute: it was considered and rejected for this
phase (AIUI-04, 13-RESEARCH.md Open Question 2). `allow-scripts`
together with `allow-same-origin` is the well-known escape pattern,
and dropping `allow-same-origin` moves AIUI to an opaque origin,
breaking its storage and its origin-checked postMessage bridge a
change bigger than this phase budgeted. The enforced boundary
instead is the /aiui/-scoped Content-Security-Policy (nginx) plus
the node-side rate limit (G-B3, 13-12); the residual risk (a
browser that ignores or partially enforces CSP) is named in
13-AI-SPEC.md §6, not silently assumed away. -->
<iframe
v-if="aiuiUrl"
ref="aiuiFrame"
@@ -38,6 +48,7 @@
:title="t('chat.aiAssistant')"
class="chat-iframe chat-iframe-mobile"
allow="microphone"
referrerpolicy="no-referrer"
style="background: transparent"
/>