[BUG] 3.8.0 server build: CSP script-src hash does not match the inline script it serves (fatal on Safari)

Author: itetsuichiCreated Sep 18, 2026Updated Sep 19, 2026

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:

bash
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:

bash
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)
EOS

It 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 show N/A and 0. 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:

nginx
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)