diff --git a/.claude/plans/reflective-meandering-castle.md b/.claude/plans/reflective-meandering-castle.md index 1b3f7482..f8a38434 100644 --- a/.claude/plans/reflective-meandering-castle.md +++ b/.claude/plans/reflective-meandering-castle.md @@ -123,7 +123,7 @@ After getting Claude Max OAuth working on the live server, hardening the deploy - **Change**: Deploy latest AIUI build. Test chat mode end-to-end. If broken, fix and redeploy. Verify: (1) AIUI loads in iframe, (2) Claude responds via proxy, (3) Context broker sends real data, (4) Close button works on mobile and desktop. - **Verify**: Full chat conversation works without errors -### Task 22: Security report +### Task 22: Security report [DONE] - **Change**: Audit all new features (cloud file upload, AIUI iframe, context broker, filebrowser proxy). Check for XSS in file names, path traversal in filebrowser-client, origin validation in context broker postMessage, CSRF in RPC endpoints. Produce report. - **Verify**: Written report with findings and mitigations diff --git a/docs/security-audit-2026-03-05.md b/docs/security-audit-2026-03-05.md new file mode 100644 index 00000000..a4db4eac --- /dev/null +++ b/docs/security-audit-2026-03-05.md @@ -0,0 +1,230 @@ +# Archipelago Security Audit Report + +**Date**: 2026-03-05 +**Scope**: Cloud file upload, AIUI iframe, context broker, FileBrowser proxy, RPC endpoints +**Auditor**: Automated code audit (Claude) + +--- + +## Executive Summary + +The Archipelago frontend is well-protected against **XSS** thanks to Vue's default template escaping. The **context broker** has correct origin validation. However, there are **path traversal risks** in the FileBrowser client, **CSRF gaps** in the RPC layer, and **token exposure** in download URLs. None are remotely exploitable without LAN access, but they should be addressed before public-facing deployment. + +| Area | Risk | Severity | +|------|------|----------| +| XSS in file names | Protected by Vue escaping | **None** | +| Context broker origin | Correctly validated | **None** | +| AIUI iframe sandbox | Properly configured | **None** | +| FileBrowser path traversal | Client-side paths not sanitized | **Medium** | +| FileBrowser token in URLs | Token exposed in query strings | **Medium** | +| CORS policy | `Access-Control-Allow-Origin: *` on some endpoints | **High** | +| CSRF tokens | No CSRF mechanism exists | **High** | +| Nginx security headers | Missing X-Frame-Options, CSP, nosniff | **Medium** | +| X-Frame-Options stripping | All app proxies strip framing protection | **Medium** | + +--- + +## 1. XSS in File Names — NO ISSUES FOUND + +All file name rendering uses Vue's `{{ }}` text interpolation, which auto-escapes HTML: + +- `CloudFolder.vue` — section names via `{{ section?.name }}` +- `FileCard.vue:34` — `{{ item.name }}` (text interpolation) +- `FileCardGrid.vue:65` — `{{ item.name }}` (text interpolation) +- `CloudToolbar.vue` — breadcrumbs via `{{ crumb.name }}` +- `Home.vue` — only numeric metrics displayed (storage bytes, folder counts) + +No use of `v-html`, `innerHTML`, or other unsafe rendering anywhere in the cloud feature. A file named `.txt` renders as literal escaped text. + +Upload handling in `CloudFolder.vue:302-308` passes raw `File` objects (not strings), and `filebrowser-client.ts:74` properly URL-encodes file names with `encodeURIComponent()`. + +**Verdict**: Safe. Vue's default escaping provides robust XSS protection. + +--- + +## 2. AIUI Iframe & Context Broker — NO ISSUES FOUND + +### Iframe Sandbox +`Chat.vue:34` uses `sandbox="allow-scripts allow-same-origin allow-forms"` — the minimum permissions needed for AIUI to function. `allow-same-origin` is required for postMessage origin validation to work. + +### Origin Validation +`contextBroker.ts:27-34` correctly derives the allowed origin: +```typescript +const url = new URL(aiuiUrl, window.location.origin) +this.allowedOrigin = url.origin +``` + +`contextBroker.ts:65` validates every incoming message: +```typescript +if (event.origin !== this.allowedOrigin) return +``` + +`contextBroker.ts:475` sends responses with explicit target origin: +```typescript +this.iframe.value.contentWindow.postMessage(msg, this.allowedOrigin) +``` + +For same-origin AIUI (production: `/aiui/`), `this.allowedOrigin` equals `window.location.origin`, which is correct. + +`Chat.vue:98-108` also validates origin for the `ready` message independently. + +**Verdict**: Properly secured. Double origin validation, explicit target origins on postMessage. + +--- + +## 3. FileBrowser Path Traversal — MEDIUM RISK + +### Finding: Paths not URL-encoded in API calls +`filebrowser-client.ts` constructs API URLs with raw path strings: + +- Line 55: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)` +- Line 69: `return \`${this.baseUrl}/api/raw${safePath}?auth=${this.token}\`` +- Line 100: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)` +- Line 127: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)` + +The `safePath` helper only prepends `/` if missing — it does NOT reject `..` sequences or canonicalize paths. + +### Mitigating Factors +1. **FileBrowser runs in a container** with volume mount `/var/lib/archipelago/filebrowser:/srv` — the daemon itself enforces path boundaries +2. **Nginx proxies** to `127.0.0.1:8083` — not externally accessible +3. **Paths come from FileBrowser API responses** (server-generated), not direct user input in most cases +4. **LAN-only access** — attacker needs network access + +### Recommendations +1. Add path validation in `filebrowser-client.ts`: + ```typescript + function sanitizePath(path: string): string { + const normalized = path.split('/').filter(p => p !== '..' && p !== '.').join('/') + return normalized.startsWith('/') ? normalized : `/${normalized}` + } + ``` +2. URL-encode path components in download URLs +3. Verify FileBrowser container uses `--read-only` filesystem + +--- + +## 4. FileBrowser Token Exposure — MEDIUM RISK + +### Finding: JWT in query parameters +`filebrowser-client.ts:69` exposes the auth token in download URLs: +```typescript +return `${this.baseUrl}/api/raw${safePath}?auth=${this.token}` +``` + +This token appears in: +- Browser history +- Nginx access logs +- HTTP Referer headers +- DOM (in `` elements) + +### Recommendation +Use the `X-Auth` header (already used for other requests at line 49) instead of query parameters. For downloads, use a short-lived download token or proxy through a backend endpoint. + +--- + +## 5. CORS Policy — HIGH RISK (LAN-scoped) + +### Finding: Wildcard CORS on multiple endpoints +`core/archipelago/src/api/handler.rs:15` defines `const CORS_ANY: &str = "*"` and applies it to: +- `/api/container/logs` (lines 108, 118) +- `/archipelago/node-message` (line 142) +- `/electrs-status` (line 153) +- `/proxy/lnd/` (lines 173, 183) + +The main `/rpc/v1` endpoint does NOT set CORS headers (more restrictive by default). + +### Mitigating Factors +1. Server is LAN-only (no public internet exposure) +2. Main RPC endpoint is not affected +3. `credentials: 'include'` with `Access-Control-Allow-Origin: *` is actually blocked by browsers (CORS spec requires specific origin when credentials are used) + +### Recommendations +1. Replace `*` with the specific Archipelago origin +2. Add `Access-Control-Allow-Credentials: true` only where needed +3. Handle OPTIONS preflight requests properly + +--- + +## 6. CSRF Protection — HIGH RISK (LAN-scoped) + +### Finding: No CSRF mechanism +- No CSRF token generation or validation +- No `X-Requested-With` custom header requirement +- No `SameSite` cookie attribute +- No `Origin` header validation in the RPC handler + +### Mitigating Factors +1. **JSON-RPC requires `Content-Type: application/json`** — this is NOT a "simple" CORS content type, so browsers send preflight OPTIONS requests for cross-origin POSTs. Since the backend returns 404 for OPTIONS, cross-origin JSON-RPC calls are effectively blocked. +2. **LAN-only access** — attacker needs to be on the same network +3. **Session cookies** — authentication appears to use session cookies from `/rpc/v1`, but an attacker on the LAN could craft a same-origin request + +### Recommendations +1. Add `X-Requested-With: XMLHttpRequest` header in `rpc-client.ts` and validate it server-side +2. Implement synchronizer token pattern for state-changing operations +3. Validate `Origin` header in the Rust handler + +--- + +## 7. Nginx Security Headers — MEDIUM RISK + +### Finding: Missing standard security headers +The nginx config lacks: +- `X-Content-Type-Options: nosniff` +- `Referrer-Policy: strict-origin-when-cross-origin` +- `Content-Security-Policy` for the main UI + +### Finding: X-Frame-Options stripped from all app proxies +Every app proxy block includes: +```nginx +proxy_hide_header X-Frame-Options; +proxy_hide_header Content-Security-Policy; +``` + +This is intentional (apps are embedded in iframes), but increases clickjacking surface. + +### Recommendations +1. Add security headers to the main location blocks: + ```nginx + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + ``` +2. Add `Content-Security-Policy` with `frame-ancestors 'self'` for the main UI +3. For app proxies, replace stripped headers with `X-Frame-Options: SAMEORIGIN` to allow Archipelago iframing but block external sites + +--- + +## Priority Action Items + +| Priority | Action | Effort | +|----------|--------|--------| +| 1 | Add `X-Requested-With` header to RPC client + validate server-side | Low | +| 2 | Add nginx security headers (nosniff, referrer-policy) | Low | +| 3 | Replace `X-Frame-Options` stripping with `SAMEORIGIN` override | Low | +| 4 | Sanitize FileBrowser paths client-side | Low | +| 5 | Move FileBrowser download auth from URL to header | Medium | +| 6 | Replace wildcard CORS with specific origins | Medium | +| 7 | Implement CSRF synchronizer tokens | High | +| 8 | Add Content-Security-Policy header | High | + +--- + +## Files Audited + +- `neode-ui/src/views/Chat.vue` +- `neode-ui/src/views/CloudFolder.vue` +- `neode-ui/src/views/Home.vue` +- `neode-ui/src/services/contextBroker.ts` +- `neode-ui/src/api/filebrowser-client.ts` +- `neode-ui/src/api/rpc-client.ts` +- `neode-ui/src/api/container-client.ts` +- `neode-ui/src/stores/cloud.ts` +- `neode-ui/src/stores/aiPermissions.ts` +- `neode-ui/src/types/aiui-protocol.ts` +- `neode-ui/src/components/cloud/FileCard.vue` +- `neode-ui/src/components/cloud/FileCardGrid.vue` +- `neode-ui/src/components/cloud/CloudToolbar.vue` +- `core/archipelago/src/api/handler.rs` +- `core/archipelago/src/api/rpc/mod.rs` +- `core/archipelago/src/api/rpc/auth.rs` +- `core/archipelago/src/api/rpc/package.rs` +- `image-recipe/configs/nginx-archipelago.conf`