[BUG] 3.8.0 server build: CSP script-src hash does not match the inline script it serves (fatal on Safari)
Summary
In the server (web) build of 3.8.0, the Content-Security-Policy header sent with index.html allows a script-src hash that does not match the inline <script> in that very same response. The inline theme-initialisation script is therefore refused by the browser.
The same response also carries no Cache-Control, which turns a one-time upgrade into a persistent failure for clients that had an older version cached.
Observed (both values taken from one response)
Started the official image with no reverse proxy in front of it:
docker run -d --name wf -e WF_SECRET_KEY=<32-byte key> -e WF_AUTH_REQUIRED=false \
-p 127.0.0.1:3399:8088 ghcr.io/wealthfolio/wealthfolio:3.8.0
curl -s -D- -o index.html http://127.0.0.1:3399/Response header:
content-security-policy: default-src 'self'; script-src 'self' 'sha256-s/UhdlprnzFxx+iXOtDj2n/Jk+MSRz1g/1lyBtFatVw=' 'wasm-unsafe-eval' blob:; ...No Cache-Control header is present.
Hash of the only inline <script> in the body of that response:
sha256-slMj4GRDTmxgcSqtC7zLnSIFInhTIO1M3Z6Gs7s7D+k=The two do not match, and script-src has neither 'unsafe-inline' nor a nonce, so the script cannot run.
Script used to compute the hash:
python3 - <<'EOS'
import re, hashlib, base64, pathlib
s = pathlib.Path("index.html").read_text()
for m in re.finditer(r"<script\b[^>]*>(.*?)</script>", s, re.S):
body = m.group(1)
if not body.strip(): # external-src tags have no inline body
continue
h = base64.b64encode(hashlib.sha256(body.encode()).digest()).decode()
print("sha256-" + h)
EOSIt looks as though the hash baked into the header was computed against a different revision of the inline script than the one shipped in the build.
Impact
The blocked script applies the cached theme before first paint.
- Chrome: the page still renders, with a theme flash. Easy to miss.
- Safari (iOS and macOS): the app does not come up. The UI shows
Backend unavailable/Wealthfolio could not load settings from the backend/Load failed, and performance widgets showN/Aand0. The backend is healthy — the failure is entirely client-side, which makes it look like a server outage.
Second, compounding issue: no Cache-Control on the HTML entry point
Because index.html is served without Cache-Control, Safari kept reusing the cached 3.6.3 page after an upgrade to 3.8.0 and requested asset filenames that no longer exist. Correcting the CSP alone did not restore the app until the entry point was also served with no-store.
Suggestion: send Cache-Control: no-store, must-revalidate (or at least no-cache) for the HTML entry point, while keeping long-lived immutable caching for the hashed assets under /assets/.
Note on 3.6.3
For what it is worth, 3.6.3 ships the same inline script (sha256-slMj4GRD...) but its script-src is just 'self' blob: — no hash at all — so the script is blocked there too. 3.8.0 looks like an attempt to fix this where the hash went stale.
Workaround currently in use
Overriding both headers at the reverse proxy:
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "... script-src 'self' 'sha256-slMj4GRDTmxgcSqtC7zLnSIFInhTIO1M3Z6Gs7s7D+k=' 'wasm-unsafe-eval' blob:; ..." always;
location = / {
add_header Cache-Control "no-store, must-revalidate" always;
}Environment
- Wealthfolio 3.8.0, server/web build, official image
ghcr.io/wealthfolio/wealthfolio:3.8.0 - Reproduced with the container alone (no reverse proxy), so this is not a proxy artefact
- Affected clients: Safari on iOS and macOS (fatal); Chrome (theme flash only)
Source: wealthfolio/wealthfolio