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>
231 lines
9.2 KiB
Markdown
231 lines
9.2 KiB
Markdown
# 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`
|