docs: add security audit report for new features (Task 22)

Audited cloud file upload, AIUI iframe, context broker, FileBrowser
proxy, and RPC endpoints. Key findings:
- XSS: safe (Vue template escaping)
- Context broker: properly validates origins
- FileBrowser: medium risk path traversal (client-side), token in URLs
- CSRF: high risk (no tokens, but mitigated by JSON content type)
- Nginx: missing security headers

Full report: docs/security-audit-2026-03-05.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-05 08:49:22 +00:00
parent 54c672943a
commit d7c9f4917a
2 changed files with 231 additions and 1 deletions

View File

@@ -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

View File

@@ -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 `<script>alert(1)</script>.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 `<a href="...">` 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`