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:
@@ -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
|
||||
|
||||
|
||||
230
docs/security-audit-2026-03-05.md
Normal file
230
docs/security-audit-2026-03-05.md
Normal 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`
|
||||
Reference in New Issue
Block a user