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