Send a CSP and security headers from the frontend's nginx
script-src admits the bundle and the pre-paint theme script by hash, with no unsafe-inline or unsafe-eval; style-src keeps unsafe-inline for the <style> elements Monaco, Radix and the toasts insert. frame-ancestors 'self', nosniff and strict-origin-when-cross-origin go with it, and the Dockerfile adds the API origin to connect-src when the bundle calls one elsewhere. Zod runs jitless so its eval probe is not reported on every page. The embed widget keeps allow-same-origin: the demo's weather map does not render without it; the comment now says so and what it costs.
This commit is contained in:
@@ -25,4 +25,11 @@ FROM nginx:1
|
||||
COPY --from=build-stage /app/frontend/dist/ /usr/share/nginx/html
|
||||
|
||||
COPY ./frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./frontend/nginx-security-headers.conf /etc/nginx/security-headers.conf
|
||||
# The CSP's connect-src names the API when the bundle calls it on another
|
||||
# origin (https://api.x and wss://api.x); empty, nginx proxies it on this one.
|
||||
ARG VITE_API_URL
|
||||
RUN api="${VITE_API_URL%/}"; \
|
||||
sed -i "s|__API_ORIGINS__|${api:+ $api $(echo "$api" | sed 's/^http/ws/')}|" \
|
||||
/etc/nginx/security-headers.conf
|
||||
COPY ./frontend/nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
|
||||
|
||||
+3
-1
@@ -11,7 +11,9 @@
|
||||
<link rel="manifest" href="%BASE_URL%manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="%BASE_URL%apple-touch-icon.png" />
|
||||
<title>Fluksio</title>
|
||||
<!-- Set the theme class before first paint so the app never flashes light. -->
|
||||
<!-- Set the theme class before first paint so the app never flashes light.
|
||||
The CSP admits this script by its hash (nginx-security-headers.conf):
|
||||
change the script and that hash changes with it. -->
|
||||
<script>
|
||||
(() => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Security headers. Included at server level and again in every location with
|
||||
# an add_header of its own, since nginx drops the inherited ones there.
|
||||
#
|
||||
# script-src: the bundle, plus the theme script inline in index.html by its
|
||||
# hash. Edit that script and this hash must change with it (sha256 of the text
|
||||
# between the tags, base64), or the app paints light-then-dark again.
|
||||
# style-src 'unsafe-inline': Monaco, Radix's scroll lock and the toasts insert
|
||||
# <style> elements at runtime, which no hash can name.
|
||||
# img/frame-src take any http(s): a camera stream or website widget points
|
||||
# wherever its dashboard says. connect-src is this origin plus the API's when
|
||||
# the bundle calls one elsewhere; the Dockerfile fills that in.
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-FpmE/jXgh7LaQmoMHvqlYVa3hl7+cMNkDN2fSejs9So='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; media-src 'self' blob:; font-src 'self'; connect-src 'self'__API_ORIGINS__; worker-src 'self'; frame-src http: https:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self'" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
@@ -24,6 +24,8 @@ server {
|
||||
text/css
|
||||
text/plain;
|
||||
|
||||
include /etc/nginx/security-headers.conf;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
@@ -35,6 +37,7 @@ server {
|
||||
# on every reload.
|
||||
location /assets/ {
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
@@ -42,6 +45,7 @@ server {
|
||||
# assets, so a stale copy pins the browser to the previous build.
|
||||
location = /index.html {
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
|
||||
@@ -474,8 +474,12 @@ function EmbedWidget({ widget }: WidgetProps) {
|
||||
<iframe
|
||||
src={url}
|
||||
title={widget.title || url}
|
||||
// Same-origin is deliberately withheld: the page keeps its own storage
|
||||
// and cookies only by being another origin, which it is.
|
||||
// `allow-same-origin` keeps the page in its own origin, with the
|
||||
// cookies and storage most sites will not start without: the demo's
|
||||
// weather map renders an unstyled list without it. That is safe only
|
||||
// while the page *is* another origin. One served from this app's own
|
||||
// origin would run as the app, scripts and all, and could read the
|
||||
// session.
|
||||
sandbox="allow-scripts allow-popups allow-forms allow-same-origin"
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Zod without its `eval` probe.
|
||||
*
|
||||
* Zod compiles a faster object parser with `new Function` where it can, and
|
||||
* finds out by trying once. Under the CSP (`frontend/nginx-security-headers.conf`)
|
||||
* that try is reported as a violation on every page, although Zod catches it
|
||||
* and falls back. `jitless` skips the probe and goes straight to the fallback.
|
||||
* Imported first in `main.tsx`, since a schema built before it would probe.
|
||||
*/
|
||||
import { z } from "zod"
|
||||
|
||||
z.config({ jitless: true })
|
||||
@@ -1,3 +1,5 @@
|
||||
// First: it configures Zod before any module builds a schema.
|
||||
import "./lib/zodConfig"
|
||||
import {
|
||||
MutationCache,
|
||||
QueryCache,
|
||||
|
||||
Reference in New Issue
Block a user