diff --git a/loop/plan.md b/loop/plan.md index 4cede188..9b33123b 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -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. diff --git a/neode-ui/package-lock.json b/neode-ui/package-lock.json index ce4bd805..5aef808b 100644 --- a/neode-ui/package-lock.json +++ b/neode-ui/package-lock.json @@ -1,14 +1,16 @@ { "name": "neode-ui", - "version": "1.1.0", + "version": "1.2.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neode-ui", - "version": "1.1.0", + "version": "1.2.0-alpha", "dependencies": { + "@types/dompurify": "^3.0.5", "d3": "^7.9.0", + "dompurify": "^3.3.3", "fast-json-patch": "^3.1.1", "fuse.js": "^7.1.0", "pinia": "^3.0.4", @@ -3809,6 +3811,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3854,7 +3865,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, "license": "MIT" }, "node_modules/@vite-pwa/assets-generator": { @@ -6194,6 +6204,15 @@ "node": ">= 8.0" } }, + "node_modules/dompurify": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/neode-ui/package.json b/neode-ui/package.json index fda82703..5895b332 100644 --- a/neode-ui/package.json +++ b/neode-ui/package.json @@ -24,7 +24,9 @@ "generate-welcome-speech": "node scripts/generate-welcome-speech.js" }, "dependencies": { + "@types/dompurify": "^3.0.5", "d3": "^7.9.0", + "dompurify": "^3.3.3", "fast-json-patch": "^3.1.1", "fuse.js": "^7.1.0", "pinia": "^3.0.4", diff --git a/neode-ui/src/api/filebrowser-client.ts b/neode-ui/src/api/filebrowser-client.ts index b5390639..1d93e8f6 100644 --- a/neode-ui/src/api/filebrowser-client.ts +++ b/neode-ui/src/api/filebrowser-client.ts @@ -59,7 +59,8 @@ class FileBrowserClient { // FileBrowser returns the JWT as a plain string (possibly quoted) this.token = text.replace(/^"|"$/g, '') // Store token as cookie for img/video/audio src requests (avoids token in URL) - document.cookie = `auth=${this.token}; path=/app/filebrowser; SameSite=Strict` + const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString() + document.cookie = `auth=${this.token}; path=/app/filebrowser; SameSite=Strict; Secure; expires=${expires}` return true } catch { return false diff --git a/neode-ui/src/components/BootScreen.vue b/neode-ui/src/components/BootScreen.vue index 5456aa7a..19f6463b 100644 --- a/neode-ui/src/components/BootScreen.vue +++ b/neode-ui/src/components/BootScreen.vue @@ -52,7 +52,7 @@