110 lines
8.6 KiB
Markdown
110 lines
8.6 KiB
Markdown
# Overnight Plan — Security Audit Remediation
|
|
|
|
> Fix every finding from the 2026-03-05 security audit (`docs/security-audit-2026-03-05.md`).
|
|
> No new features, no design changes. Pure security hardening.
|
|
> Deploy after every change: `./scripts/deploy-to-target.sh --live` — test at http://192.168.1.228
|
|
> See `CLAUDE.md` for all project rules and conventions.
|
|
|
|
---
|
|
|
|
## Phase 1: Low-Effort / High-Impact Fixes
|
|
|
|
- [ ] **Add X-Requested-With header to RPC client + validate server-side**: In `neode-ui/src/api/rpc-client.ts`, add `'X-Requested-With': 'XMLHttpRequest'` to the headers object in the `call()` method's `fetch()` options. Then in `core/archipelago/src/api/rpc/mod.rs`, find where the RPC POST handler processes requests and add a check: if the `X-Requested-With` header is missing or not `XMLHttpRequest`, return 403 Forbidden. This blocks cross-origin form submissions that bypass CORS preflight. Also add the same header to `neode-ui/src/api/container-client.ts` if it makes direct fetch calls. Run `cargo clippy` on the server after the Rust change. Test by visiting http://192.168.1.228 and verifying login + container start/stop still work.
|
|
|
|
- [ ] **Add nginx security headers (nosniff, referrer-policy)**: Edit `image-recipe/configs/nginx-archipelago.conf`. In the main `server` block (before the `location` blocks), add these headers:
|
|
```
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header X-DNS-Prefetch-Control "off" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
```
|
|
Also add the same headers to the HTTPS server block in `image-recipe/configs/snippets/archipelago-https-app-proxies.conf` if it has a separate server block. After editing, deploy to .228 and verify headers appear: `curl -sI http://192.168.1.228 | grep -i "x-content-type\|referrer-policy"`. Then rsync the nginx config to the live server: `sudo cp ~/archy/image-recipe/configs/nginx-archipelago.conf /etc/nginx/sites-available/archipelago && sudo nginx -t && sudo systemctl reload nginx`.
|
|
|
|
- [ ] **Replace X-Frame-Options stripping with SAMEORIGIN override**: In `image-recipe/configs/nginx-archipelago.conf`, find all `proxy_hide_header X-Frame-Options;` lines inside app proxy location blocks (e.g., `/app/mempool/`, `/app/btcpay/`, etc.). Replace each `proxy_hide_header X-Frame-Options;` with:
|
|
```
|
|
proxy_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
```
|
|
This allows Archipelago to iframe the apps but blocks external sites from framing them. Do the same in the HTTPS config at `image-recipe/configs/snippets/archipelago-https-app-proxies.conf`. Test by opening an app in the Archipelago UI iframe — it should still load. Then try loading the app URL directly in an iframe from a different origin — it should be blocked. Deploy nginx config to .228 and reload.
|
|
|
|
- [ ] **Sanitize FileBrowser paths client-side**: In `neode-ui/src/api/filebrowser-client.ts`, add a `sanitizePath` function near the top:
|
|
```typescript
|
|
function sanitizePath(path: string): string {
|
|
const segments = path.split('/').filter(s => s !== '..' && s !== '.' && s !== '')
|
|
return '/' + segments.join('/')
|
|
}
|
|
```
|
|
Then replace all uses of the `safePath` helper (or the raw path variable) with `sanitizePath(path)` in these methods: `list()`, `getDownloadUrl()`, `upload()`, `createFolder()`, `rename()`, `delete()`. Search for `safePath` in the file and update each occurrence. Run `cd neode-ui && npm run type-check` to verify. Test by browsing Cloud folders in the UI — navigation, downloads, uploads should all still work.
|
|
|
|
---
|
|
|
|
## Phase 2: Medium-Effort Fixes
|
|
|
|
- [ ] **Move FileBrowser download auth from URL to header-based proxy**: Currently `filebrowser-client.ts:69` exposes the JWT token in download URLs (`?auth=${this.token}`). Fix by adding an nginx location block that proxies download requests and injects the auth header server-side:
|
|
1. In `image-recipe/configs/nginx-archipelago.conf`, add a new location block:
|
|
```nginx
|
|
location /api/cloud/download {
|
|
internal;
|
|
proxy_pass http://127.0.0.1:8083/api/raw$arg_path;
|
|
proxy_set_header X-Auth $arg_token;
|
|
proxy_hide_header X-Frame-Options;
|
|
}
|
|
```
|
|
2. In `filebrowser-client.ts`, change `getDownloadUrl()` to return `/api/cloud/download?path=${encodeURIComponent(path)}&token=${this.token}` — the token goes to nginx only, not exposed in browser history or referer headers.
|
|
3. Alternatively, create a backend proxy endpoint in `core/archipelago/src/api/handler.rs` that reads the path, fetches from FileBrowser with the token in headers, and streams back.
|
|
Test by downloading a file from Cloud — it should work without the token appearing in the browser URL bar.
|
|
|
|
- [ ] **Replace wildcard CORS with specific origins**: In `core/archipelago/src/api/handler.rs`, find `const CORS_ANY: &str = "*"` and all places it's used. Replace the wildcard with the actual requesting origin if it matches an allowlist. Add a helper function:
|
|
```rust
|
|
fn cors_origin(req: &Request<hyper::Body>) -> String {
|
|
req.headers()
|
|
.get("Origin")
|
|
.and_then(|v| v.to_str().ok())
|
|
.filter(|o| o.starts_with("http://192.168.") || o.starts_with("https://192.168.") || o.starts_with("http://localhost") || o.starts_with("http://100."))
|
|
.unwrap_or("")
|
|
.to_string()
|
|
}
|
|
```
|
|
Use this to set `Access-Control-Allow-Origin` to the requesting origin (only if it matches) instead of `*`. Apply to all endpoints that currently use `CORS_ANY`: `/api/container/logs`, `/archipelago/node-message`, `/proxy/lnd/`. Add proper `OPTIONS` preflight handling for these endpoints (return 204 with CORS headers). Run `cargo clippy` after changes. Test by verifying container logs, node messages, and LND proxy still work in the UI.
|
|
|
|
---
|
|
|
|
## Phase 3: High-Effort Fixes
|
|
|
|
- [ ] **Implement CSRF synchronizer token pattern**: Two-part change:
|
|
|
|
**Backend (Rust)**:
|
|
1. In `core/archipelago/src/api/rpc/auth.rs`, when a session is created (login), generate a random CSRF token using `rand::Rng` and `hex::encode`.
|
|
2. Store the CSRF token alongside the session in the session map.
|
|
3. Return the CSRF token in the login response JSON: `"csrf_token": csrf_token`.
|
|
4. In `core/archipelago/src/api/rpc/mod.rs`, for all state-changing RPC methods, validate that the `X-CSRF-Token` header matches the session's stored token. Return 403 if missing/mismatched.
|
|
5. Exempt `auth.login` from CSRF validation.
|
|
|
|
**Frontend (Vue)**:
|
|
1. In the login handler, store the CSRF token from the response: `localStorage.setItem('csrf_token', res.csrf_token)`.
|
|
2. In `neode-ui/src/api/rpc-client.ts`, add `'X-CSRF-Token': localStorage.getItem('csrf_token') || ''` to every request header.
|
|
3. On 403 CSRF error responses, redirect to login.
|
|
|
|
Test by logging in and performing actions (start/stop containers, change settings). Then try crafting a cross-origin POST to `/rpc/v1` — it should fail with 403.
|
|
|
|
- [ ] **Add Content-Security-Policy header**: In `image-recipe/configs/nginx-archipelago.conf`, add a CSP header to the main UI location block. Start with report-only:
|
|
```nginx
|
|
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-src 'self'; frame-ancestors 'self';" always;
|
|
```
|
|
Deploy and check browser console for CSP violations. Fix violations by adjusting the policy. Once clean, change to enforcing `Content-Security-Policy`. Do NOT apply CSP to app proxy locations — those are third-party apps with different needs.
|
|
|
|
---
|
|
|
|
## Post-Fix Verification
|
|
|
|
- [ ] **Run full security re-audit**: After all fixes, verify each finding:
|
|
1. `curl -sI http://192.168.1.228` shows X-Content-Type-Options, Referrer-Policy headers
|
|
2. `curl -sI http://192.168.1.228/app/mempool/` shows X-Frame-Options: SAMEORIGIN
|
|
3. RPC calls include X-Requested-With header (check browser DevTools)
|
|
4. FileBrowser download URLs don't contain auth tokens
|
|
5. CORS responses use specific origin, not `*`: `curl -H "Origin: http://evil.com" http://192.168.1.228/api/container/logs` — should NOT return `Access-Control-Allow-Origin: *`
|
|
6. CSRF token present in RPC requests
|
|
7. All UI features still work (login, apps, cloud, web5, settings)
|
|
|
|
- [ ] **Deploy to all nodes**: After verification on .228, deploy to Arch 1 (100.82.97.63), Arch 2 (100.122.84.60), Arch 3 (100.124.105.113), .198 (192.168.1.198). Verify health on each: `curl -sI http://{ip}/ | grep -i "x-content-type\|referrer"`
|