# 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`