fix(server): allow required CSP handoffs

Permit GitHub App manifest form posts and signed HTTPS VNC preview iframes while keeping the rest of the SPA CSP locked down. Mirror the policy in the split-web Caddy config.
This commit is contained in:
Bryan Helmkamp 2026-06-01 18:24:45 -04:00
parent 4a156d5551
commit 3e881e9938
No known key found for this signature in database
2 changed files with 16 additions and 5 deletions

View file

@ -9,7 +9,7 @@
Cross-Origin-Opener-Policy same-origin Cross-Origin-Opener-Policy same-origin
Cross-Origin-Resource-Policy same-origin Cross-Origin-Resource-Policy same-origin
Permissions-Policy "accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), usb=(), web-share=(), xr-spatial-tracking=()" Permissions-Policy "accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), usb=(), web-share=(), xr-spatial-tracking=()"
Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://avatars.githubusercontent.com; connect-src 'self' ws: wss:; worker-src 'self' blob:; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://avatars.githubusercontent.com; connect-src 'self' ws: wss:; worker-src 'self' blob:; frame-src 'self' https:; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' https://github.com; object-src 'none'"
} }
@sourceMaps path *.map @sourceMaps path *.map

View file

@ -2,8 +2,7 @@
//! //!
//! The policy is built once at server startup from the embedded SPA //! The policy is built once at server startup from the embedded SPA
//! `index.html` so any inline `<script>` hashes don't drift from the //! `index.html` so any inline `<script>` hashes don't drift from the
//! template. Third-party sources are enumerated explicitly — the only //! template. Third-party sources are enumerated explicitly.
//! outside origins the UI depends on today are Google Fonts.
use std::sync::OnceLock; use std::sync::OnceLock;
@ -91,7 +90,10 @@ fn build_policy_with_hashes(script_hashes: &[String]) -> String {
// interactions. The meaningful XSS protection still comes from the // interactions. The meaningful XSS protection still comes from the
// script-src restrictions above. `ws:`/`wss:` keep the same-origin // script-src restrictions above. `ws:`/`wss:` keep the same-origin
// terminal WebSocket working across browsers that do not treat `'self'` // terminal WebSocket working across browsers that do not treat `'self'`
// as matching WebSocket schemes for connect-src. // as matching WebSocket schemes for connect-src. `frame-src https:`
// allows signed sandbox VNC preview iframes from dynamic Daytona preview
// hosts. `https://github.com` in form-action allows the install-mode
// GitHub App manifest POST handoff.
format!( format!(
"default-src 'self'; \ "default-src 'self'; \
script-src 'self'{inline_script_sources} 'wasm-unsafe-eval'; \ script-src 'self'{inline_script_sources} 'wasm-unsafe-eval'; \
@ -100,10 +102,11 @@ fn build_policy_with_hashes(script_hashes: &[String]) -> String {
img-src 'self' data: blob: https://avatars.githubusercontent.com; \ img-src 'self' data: blob: https://avatars.githubusercontent.com; \
connect-src 'self' ws: wss:; \ connect-src 'self' ws: wss:; \
worker-src 'self' blob:; \ worker-src 'self' blob:; \
frame-src 'self' https:; \
manifest-src 'self'; \ manifest-src 'self'; \
frame-ancestors 'none'; \ frame-ancestors 'none'; \
base-uri 'self'; \ base-uri 'self'; \
form-action 'self'; \ form-action 'self' https://github.com; \
object-src 'none'" object-src 'none'"
) )
} }
@ -165,6 +168,14 @@ mod tests {
assert!(policy.contains("font-src 'self' https://fonts.gstatic.com")); assert!(policy.contains("font-src 'self' https://fonts.gstatic.com"));
assert!(policy.contains("style-src 'self' https://fonts.googleapis.com 'unsafe-inline'")); assert!(policy.contains("style-src 'self' https://fonts.googleapis.com 'unsafe-inline'"));
assert!(policy.contains("connect-src 'self' ws: wss:")); assert!(policy.contains("connect-src 'self' ws: wss:"));
assert!(
policy.contains("frame-src 'self' https:"),
"signed sandbox VNC previews are embedded from dynamic HTTPS origins"
);
assert!(
policy.contains("form-action 'self' https://github.com"),
"GitHub App manifest creation posts directly to github.com"
);
assert!(policy.contains("frame-ancestors 'none'")); assert!(policy.contains("frame-ancestors 'none'"));
assert!(policy.contains("object-src 'none'")); assert!(policy.contains("object-src 'none'"));
} }