Security hardening
This guide covers H-09 / M-17 hardening shipped incrementally in the shell:
- Reduced token persistence blast radius (default, no config required)
- Optional same-origin BFF for HttpOnly refresh cookies
- Shell Content-Security-Policy with inline script hash (staged report-only by default)
Companion sandbox isolation (#65) is out of scope here. For postMessage origin allowlists (security.allowedMessageOrigins), see the postMessage hardening notes in the changelog and Authentication.
Token storage (default)
Without any security config, the shell now splits persisted auth state:
| Location | Key | Contents |
|---|---|---|
localStorage | shellui.auth.profile | User profile + expiry metadata (no tokens) |
sessionStorage | shellui.auth.refresh | Refresh token (tab-scoped) |
| Memory (React) | — | Access token only |
Legacy shellui.auth.session blobs are migrated automatically on first read.
Residual risk (default mode)
- Refresh tokens remain JavaScript-readable inside the tab (
sessionStorage). XSS in the shell can still exfiltrate them until you enable BFF. - Access tokens are not written to web storage, but they still live in memory and may be forwarded to trusted iframe companions via
SHELLUI_SETTINGS(see Navigation →safeForAuthToken). - New tabs do not inherit
sessionStorage; users may need to sign in again unless BFF HttpOnly cookies are enabled.
BFF auth (HttpOnly refresh) — phased
Enable when the shell is served from an origin that can host /api/auth/* (CLI dev server, serve:dist, or your production reverse proxy):
{
"backend": {
"type": "shellui",
"url": "http://localhost:8000",
"companyId": 1
},
"security": {
"bffAuth": {
"enabled": true
}
}
}
What works today
| Route | Purpose |
|---|---|
POST /api/auth/session | Accept refresh once after OAuth/hash login; sets HttpOnly shellui_refresh cookie |
POST /api/auth/refresh | Rotate access token server-side; refresh never returned to JS |
POST /api/auth/logout | Clears cookie (+ identity logout when possible) |
The CLI wires these in shellui start and pnpm run serve:dist.
Production follow-ups (not in this repo)
- Identity-service cookie endpoints — today the BFF proxies existing
/api/v1/tokenrefresh. A native cookie session API on identity would remove the one-time JS hand-off on login. - OAuth callback without hash tokens — ideal flow: identity sets cookie → redirects shell without
#access_token=…. Requires identity/hosting changes. - CDN / edge — replicate
/api/auth/*on your edge if the static shell and API share a public origin. Securecookies — setSHELLUI_BFF_SECURE_COOKIE=1(or serve over HTTPS) in production.
Shell CSP
CSP headers are emitted by:
shellui start(dev)pnpm run serve:dist(production preview)
Default policy characteristics:
- Inline theme bootstrap allowed via SHA-256 hash (see
packages/cli/src/utils/csp.js, kept in sync withindex.html) script-src 'self' 'sha256-…'- Permissive
frame-srcfor localhost companions during rollout connect-srcincludesbackend.urlwhen configured
Staged rollout
By default the shell sends Content-Security-Policy-Report-Only so playgrounds keep working while you collect violations.
{
"security": {
"csp": {
"reportOnly": true,
"reportUri": "https://reports.example.com/csp"
}
}
}
When clean, switch to enforcing:
{
"security": {
"csp": {
"enforce": true,
"reportOnly": false
}
}
}
Add companion origins explicitly if needed:
{
"security": {
"csp": {
"connectSrc": ["http://localhost:5173"],
"frameSrc": ["http://localhost:3000"]
}
}
}
If you serve dist/web/ from a CDN without Shellui middleware, replicate the same header (hash from computeThemeInitScriptHash() in @shellui/cli utils or your build pipeline).
Verification checklist
- Sign in — confirm
localStoragehasshellui.auth.profilewithout token fields. - With default mode — refresh token appears under
sessionStoragekeyshellui.auth.refreshonly. - Enable
security.bffAuth.enabled— after login, refresh token should not appear in web storage;/api/auth/refreshreturns a new access token. - DevTools → Network — HTML responses include
Content-Security-Policy-Report-Only(unless enforcing). - Run unit tests:
pnpm test(core auth persistence + CLI CSP helpers).