fix: Phase 5 — XSS sanitization, cookie security, redirect validation, input trimming

- BootScreen + Settings: v-html now uses DOMPurify.sanitize() for SVG content
- FileBrowser cookie: added Secure flag and 24h expiration
- TOTP secret: hidden by default with reveal toggle button
- Login redirect: validates URL is local-origin before redirecting
- Auth fields: password inputs trimmed before submission
- Route params: appId validated against safe pattern, invalid IDs redirect to /apps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 00:55:00 +00:00
parent 5853b6a065
commit 3418c273d4
10 changed files with 84 additions and 24 deletions

View File

@@ -378,7 +378,7 @@
> into the page (XSS), steal login cookies, or redirect you to a fake site after login. We fix all
> of these and add proper input sanitization everywhere.
- [ ] **Fix v-html XSS in BootScreen and Settings**: In `neode-ui/src/components/BootScreen.vue` line 55, replace `v-html="icons[currentIcon]"` with a safe rendering approach:
- [x] **Fix v-html XSS in BootScreen and Settings**: In `neode-ui/src/components/BootScreen.vue` line 55, replace `v-html="icons[currentIcon]"` with a safe rendering approach:
1. Since the icons are hardcoded SVG strings, create a computed property that returns the current icon and use `v-html` with a DOMPurify sanitizer.
2. Install DOMPurify: `cd neode-ui && npm install dompurify && npm install -D @types/dompurify`.
3. Verify the package exists first: `npm view dompurify version`.
@@ -395,7 +395,7 @@
6. Run `npm run type-check` to verify.
7. Build and deploy. Verify boot screen animation still works. Verify TOTP QR code still renders on Settings page.
- [ ] **Fix FileBrowser cookie security flags**: In `neode-ui/src/api/filebrowser-client.ts` line 62, find `document.cookie = \`auth=${this.token}; path=/app/filebrowser; SameSite=Strict\``. This cookie is missing security flags. Since we can't set `HttpOnly` from JavaScript (that's a server-side flag), the best we can do client-side is:
- [x] **Fix FileBrowser cookie security flags**: In `neode-ui/src/api/filebrowser-client.ts` line 62, find `document.cookie = \`auth=${this.token}; path=/app/filebrowser; SameSite=Strict\``. This cookie is missing security flags. Since we can't set `HttpOnly` from JavaScript (that's a server-side flag), the best we can do client-side is:
```typescript
document.cookie = `auth=${this.token}; path=/app/filebrowser; SameSite=Strict; Secure`
```
@@ -407,7 +407,7 @@
```
Build and deploy. Verify FileBrowser still works (login, browse, download).
- [ ] **Hide TOTP secret by default**: In `neode-ui/src/views/Settings.vue`, find line 289 with `{{ totpSecretBase32 }}`. Wrap it in a reveal toggle:
- [x] **Hide TOTP secret by default**: In `neode-ui/src/views/Settings.vue`, find line 289 with `{{ totpSecretBase32 }}`. Wrap it in a reveal toggle:
1. Add a ref: `const showTotpSecret = ref(false)`
2. Replace the display with:
```vue
@@ -425,7 +425,7 @@
3. Remove the `select-all` class — users should deliberately copy, not accidentally select.
Build and deploy. Verify TOTP setup flow still works.
- [ ] **Validate redirect URL after login**: In `neode-ui/src/router/index.ts`, find line 231 with `const redirectTo = (to.query.redirect as string) || '/dashboard'`. Replace with:
- [x] **Validate redirect URL after login**: In `neode-ui/src/router/index.ts`, find line 231 with `const redirectTo = (to.query.redirect as string) || '/dashboard'`. Replace with:
```typescript
function isLocalRedirect(path: unknown): path is string {
if (typeof path !== 'string') return false
@@ -443,13 +443,13 @@
```
Run `npm run type-check`. Build and deploy. Test: visit `http://192.168.1.198/login?redirect=https://evil.com` — after login should go to `/dashboard`, NOT `evil.com`. Visit `http://192.168.1.198/login?redirect=/mesh` — after login should go to `/mesh`.
- [ ] **Add input trimming to all auth fields**: In `neode-ui/src/views/Login.vue`, find all password and input submissions. Add `.trim()` before sending:
- [x] **Add input trimming to all auth fields**: In `neode-ui/src/views/Login.vue`, find all password and input submissions. Add `.trim()` before sending:
1. Search for `password.value` in the file. Wherever it's submitted via RPC (e.g., `params: { password: password.value }`), change to `params: { password: password.value.trim() }`.
2. Do the same for TOTP code inputs, setup passwords, confirm passwords.
3. Also check `neode-ui/src/views/Settings.vue` for password change forms — trim those too.
Run `npm run type-check`. Build and deploy. Test login with a password that has trailing spaces — should still work.
- [ ] **Validate route parameters**: In `neode-ui/src/views/AppDetails.vue` (line ~485) and `neode-ui/src/views/AppSession.vue` (line ~267), add app ID validation:
- [x] **Validate route parameters**: In `neode-ui/src/views/AppDetails.vue` (line ~485) and `neode-ui/src/views/AppSession.vue` (line ~267), add app ID validation:
1. Create a utility function in `neode-ui/src/utils/` or inline:
```typescript
function isValidAppId(id: unknown): id is string {
@@ -469,7 +469,7 @@
```
Build and deploy. Test: navigate to a valid app — should work. Navigate to `/app/../../etc/passwd` — should redirect to `/apps`.
- [ ] **Verify Phase 5 — Frontend hardened**: Run these checks:
- [x] **Verify Phase 5 — Frontend hardened**: Run these checks:
1. `grep -rn "v-html" neode-ui/src/ --include="*.vue" | grep -v "DOMPurify\|sanitize"` — any remaining v-html should be justified.
2. `grep -rn "select-all" neode-ui/src/ --include="*.vue"` — TOTP secret should NOT have select-all.
3. `npm run type-check` — zero errors.