fix(boardwalk): add form-action and frame-src to the admin CSP
- form-action 'self': injected markup cannot post a native form off-origin - frame-src 'none': no framed content; a sandboxed srcdoc preview is not fetched and still renders - header tests, README and admin-spa docs name the directives
This commit is contained in:
@@ -15,7 +15,7 @@ cabana mounts the SPA under the admin prefix, `backend.uri` (`/backend` by defau
|
||||
- Any path under the prefix that is not a file and not under `api/` returns `index.html`, so the SPA's own routes work on reload.
|
||||
- Paths under `api/` that no API route matches return the admin API's JSON `not_found` error, never the SPA.
|
||||
- Hashed files under `assets/` are cached for a long time; `index.html` is never cached.
|
||||
- Every response carries a restrictive Content-Security-Policy, frame denial, `nosniff`, a same-origin referrer policy and `noindex, nofollow`.
|
||||
- Every response carries a restrictive Content-Security-Policy (`frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; form-action 'self'; frame-src 'none'`: scripts only from the admin origin, no native form post to another origin, no framed content), frame denial, `nosniff`, a same-origin referrer policy and `noindex, nofollow`.
|
||||
|
||||
The build is path-agnostic: `index.html` holds a placeholder (`boardwalk.BaseToken`) that `boardwalk.RewriteIndex` replaces with the prefix once, when the handler is built. A build without the placeholder fails the start-up.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Use cases:
|
||||
- Falls back to `index.html` for client-side routes and directories; missing files with an extension get a plain 404.
|
||||
- Hands every request whose path under the prefix is `api` or starts with `api/` to a caller-supplied handler, so admin API misses stay JSON instead of returning the SPA.
|
||||
- Long-lived immutable caching for hashed files under `assets/`, `no-cache` for other files and `no-store` for `index.html`.
|
||||
- Security headers on every response: a restrictive Content-Security-Policy, frame denial, `nosniff`, a same-origin referrer policy and `noindex, nofollow`.
|
||||
- Security headers on every response: a restrictive Content-Security-Policy (`frame-ancestors 'none'`, `base-uri 'none'`, `object-src 'none'`, `script-src 'self'`, `form-action 'self'`, `frame-src 'none'`), frame denial, `nosniff`, a same-origin referrer policy and `noindex, nofollow`.
|
||||
- Explicit content types for scripts, styles, fonts, SVG and JSON, with a MIME lookup fallback.
|
||||
|
||||
## Usage
|
||||
@@ -55,7 +55,7 @@ mux.Handle("/backend/", spa)
|
||||
| `boardwalk.RewriteIndex` | Rewrites raw `index.html` bytes for a prefix; errors when the placeholder token is missing. |
|
||||
| `boardwalk.BaseToken` | The placeholder in `dist/index.html` that is replaced by the prefix. |
|
||||
| `boardwalk.ContentType` | The Content-Type served for a file name, with explicit UTF-8 JavaScript and CSS types. Shared with the plugin asset route in cabana. |
|
||||
| `boardwalk.SetSecurityHeaders` | Sets the admin security headers (nosniff, referrer policy, no framing, the `script-src 'self'` CSP, noindex) on a response. |
|
||||
| `boardwalk.SetSecurityHeaders` | Sets the admin security headers (nosniff, referrer policy, no framing, the CSP with `script-src 'self'`, `form-action 'self'` and `frame-src 'none'`, noindex) on a response. |
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@@ -28,8 +28,11 @@ var distFS embed.FS
|
||||
const BaseToken = "__SUMMER_ADMIN_BASE__"
|
||||
|
||||
// contentSecurityPolicy keeps the admin out of frames and allows scripts
|
||||
// only from its own origin; the build contains no inline script.
|
||||
const contentSecurityPolicy = "frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'"
|
||||
// only from its own origin; the build contains no inline script. Injected
|
||||
// markup can neither post a native form off-origin (form-action 'self') nor
|
||||
// frame fetched content (frame-src 'none'); a sandboxed srcdoc preview is not
|
||||
// fetched, so it still renders.
|
||||
const contentSecurityPolicy = "frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; form-action 'self'; frame-src 'none'"
|
||||
|
||||
var contentTypes = map[string]string{
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
@@ -164,7 +167,8 @@ func ContentType(name string) string {
|
||||
|
||||
// SetSecurityHeaders sets the admin response headers: nosniff, a same-origin
|
||||
// referrer policy, no framing, the admin Content-Security-Policy
|
||||
// (script-src 'self') and noindex.
|
||||
// (script-src 'self', form-action 'self', frame-src 'none': no native form
|
||||
// post off-origin, no framed content) and noindex.
|
||||
func SetSecurityHeaders(h http.Header) {
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Referrer-Policy", "same-origin")
|
||||
|
||||
@@ -310,7 +310,7 @@ func TestSecurityHeadersOnEveryResponse(t *testing.T) {
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "same-origin",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Content-Security-Policy": "frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'",
|
||||
"Content-Security-Policy": "frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; form-action 'self'; frame-src 'none'",
|
||||
"X-Robots-Tag": "noindex, nofollow",
|
||||
} {
|
||||
if got := rec.Header().Get(header); got != want {
|
||||
@@ -482,7 +482,7 @@ func TestPhase101BoardwalkExports(t *testing.T) {
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "same-origin",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Content-Security-Policy": "frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'",
|
||||
"Content-Security-Policy": "frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; form-action 'self'; frame-src 'none'",
|
||||
"X-Robots-Tag": "noindex, nofollow",
|
||||
}
|
||||
if len(h) != len(want) {
|
||||
|
||||
Reference in New Issue
Block a user