Overnight pentest run produced recon, analysis, exploitation reports, and a full security assessment. Plan.md updated with 22 prioritized fix items for auth, SSRF, injection, XSS, and hardening. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
240 lines
11 KiB
Markdown
240 lines
11 KiB
Markdown
I now have all the data needed. Let me compile the comprehensive XSS analysis.
|
|
|
|
---
|
|
|
|
# XSS Vulnerability Analysis — Archipelago (192.168.1.228)
|
|
|
|
## Methodology
|
|
|
|
Source code review of the full stack: Rust backend (`core/`), Vue 3 frontend (`neode-ui/src/`), Nginx configs (`image-recipe/configs/`), and static HTML files. Searched for all XSS sinks (v-html, innerHTML, document.write, eval), DOM-based sources (location, postMessage), stored content rendering, and missing security headers.
|
|
|
|
---
|
|
|
|
## XSS-001 — Stored XSS via Unauthenticated P2P Node Messages
|
|
**Type**: Stored XSS
|
|
**Confidence**: Medium
|
|
**Location**: `POST /archipelago/node-message` → rendered in Web5.vue and App.vue toast
|
|
|
|
**Source files**:
|
|
- Backend intake: `core/archipelago/src/api/handler.rs:125-145` — no auth, no sanitization
|
|
- Backend storage: `core/archipelago/src/node_message.rs:26-37` — raw string stored as-is
|
|
- Frontend display (messages): `neode-ui/src/views/Web5.vue:405` — `{{ m.message }}`
|
|
- Frontend display (toast): `neode-ui/src/App.vue:52` — `{{ toastMessage.text }}`
|
|
- Toast data source: `neode-ui/src/composables/useMessageToast.ts:39` — `latest?.message`
|
|
|
|
**Evidence**: The `/archipelago/node-message` endpoint accepts arbitrary JSON with `from_pubkey` and `message` fields — no authentication, no input validation, no HTML sanitization. Messages are stored in memory and returned verbatim via the `node-messages-received` RPC method. The frontend renders messages using Vue's `{{ }}` text interpolation, which **does** escape HTML by default. However:
|
|
|
|
1. The toast at `App.vue:52` renders the raw message text as a notification preview — if Vue's escaping were ever bypassed (e.g., a future refactor introduces `v-html`), this becomes immediately exploitable
|
|
2. The `:title` attribute binding at `Web5.vue:402` (`<p ... :title="m.from_pubkey">`) accepts the unsanitized pubkey — attribute injection is possible with crafted pubkey values
|
|
3. Combined with **CORS wildcard** (`Access-Control-Allow-Origin: *` on all endpoints), any website can inject messages via a drive-by attack
|
|
|
|
**Why medium, not high**: Vue's `{{ }}` escaping prevents current exploitation. But the complete absence of server-side sanitization means any rendering change (or alternative client) would be immediately vulnerable.
|
|
|
|
**Suggested exploit**:
|
|
```bash
|
|
curl -X POST http://192.168.1.228/archipelago/node-message \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"from_pubkey":"\" onfocus=alert(1) autofocus=\"","message":"<img src=x onerror=alert(document.cookie)>"}'
|
|
```
|
|
|
|
---
|
|
|
|
## XSS-002 — postMessage Origin Bypass in AppLauncherOverlay
|
|
**Type**: DOM-based XSS (postMessage sink)
|
|
**Confidence**: Medium
|
|
**Location**: `neode-ui/src/components/AppLauncherOverlay.vue:125,147-150`
|
|
|
|
**Source file**: `neode-ui/src/components/AppLauncherOverlay.vue`
|
|
|
|
**Evidence**:
|
|
- Line 125: `window.parent.postMessage({ type: 'app-launcher-escape' }, '*')` — sends to ANY origin
|
|
- Lines 147-150: Receives messages with **no origin validation**:
|
|
```typescript
|
|
function onMessage(e: MessageEvent) {
|
|
if (e.data?.type === 'app-launcher-escape' && store.isOpen) {
|
|
store.close()
|
|
}
|
|
}
|
|
```
|
|
- Any page embedding the Archipelago UI (or any malicious iframe loaded into the app launcher) can trigger the close action. The impact is UI manipulation only (closing the app launcher), but this pattern demonstrates missing origin checks that could be exploited if more actions are added.
|
|
|
|
**Suggested exploit**: From a malicious page iframed into the app launcher:
|
|
```javascript
|
|
window.parent.postMessage({ type: 'app-launcher-escape' }, '*')
|
|
```
|
|
|
|
---
|
|
|
|
## XSS-003 — postMessage Origin Bypass in Claude Auth Handler
|
|
**Type**: DOM-based XSS (postMessage sink)
|
|
**Confidence**: Medium
|
|
**Location**: `neode-ui/src/views/Settings.vue:442-448`
|
|
|
|
**Source file**: `neode-ui/src/views/Settings.vue`
|
|
|
|
**Evidence**:
|
|
```typescript
|
|
function handleClaudeLoginMessage(e: MessageEvent) {
|
|
if (e.data?.type === 'claude-auth-success') {
|
|
claudeConnected.value = true
|
|
showClaudeLoginModal.value = false
|
|
window.removeEventListener('message', handleClaudeLoginMessage)
|
|
}
|
|
}
|
|
```
|
|
No `e.origin` validation. Any iframe or window (including apps loaded in the app launcher) can send `{ type: 'claude-auth-success' }` to spoof the Claude connection state. This is UI spoofing — the user sees "Claude Connected" when it's not authenticated.
|
|
|
|
**Suggested exploit**:
|
|
```javascript
|
|
// From any page loaded in the same browsing context
|
|
window.postMessage({ type: 'claude-auth-success' }, '*')
|
|
```
|
|
|
|
---
|
|
|
|
## XSS-004 — Absent Content-Security-Policy + CSP/X-Frame-Options Stripping
|
|
**Type**: Missing security headers (XSS enabler)
|
|
**Confidence**: High
|
|
**Location**: `image-recipe/configs/nginx-archipelago.conf` (entire server block)
|
|
|
|
**Source file**: `image-recipe/configs/nginx-archipelago.conf:89-333`
|
|
|
|
**Evidence**:
|
|
1. **No CSP header** set on any response — no defense-in-depth against XSS
|
|
2. **No X-Frame-Options** — clickjacking possible on main UI
|
|
3. **No X-Content-Type-Options** — MIME sniffing attacks possible
|
|
4. 25+ app proxy locations explicitly strip CSP and X-Frame-Options:
|
|
```nginx
|
|
proxy_hide_header X-Frame-Options;
|
|
proxy_hide_header Content-Security-Policy;
|
|
```
|
|
This removes the security headers that apps like Vaultwarden, Portainer, and Grafana set to protect themselves, making them vulnerable to clickjacking when proxied.
|
|
|
|
Without CSP, if any XSS vector is found (including in proxied apps), there are zero mitigations — inline scripts, eval, and external script loading all work.
|
|
|
|
---
|
|
|
|
## XSS-005 — Echo Endpoint Reflects Arbitrary Input
|
|
**Type**: Reflected (JSON context)
|
|
**Confidence**: Low
|
|
**Location**: `POST /rpc/v1` method `echo` / `server.echo`
|
|
|
|
**Source file**: `core/archipelago/src/api/rpc/mod.rs:171-178`
|
|
|
|
**Evidence**:
|
|
```rust
|
|
async fn handle_echo(&self, params: Option<serde_json::Value>) -> Result<serde_json::Value> {
|
|
if let Some(p) = params {
|
|
if let Some(msg) = p.get("message").and_then(|v| v.as_str()) {
|
|
return Ok(serde_json::json!({ "message": msg }));
|
|
}
|
|
}
|
|
Ok(serde_json::json!({ "message": "Hello from Archipelago!" }))
|
|
}
|
|
```
|
|
The `message` parameter is reflected verbatim in the JSON response. The response has `Content-Type: application/json`, so browsers won't render it as HTML. However, combined with CORS wildcard, any website can read the reflected value. If this response is ever consumed unsafely by the frontend or a third-party client, XSS is possible.
|
|
|
|
**Suggested exploit**:
|
|
```bash
|
|
curl -X POST http://192.168.1.228/rpc/v1 \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":1,"method":"echo","params":{"message":"<script>alert(1)</script>"}}'
|
|
```
|
|
|
|
---
|
|
|
|
## XSS-006 — test-aiui.html postMessage Without Origin Validation + innerHTML
|
|
**Type**: DOM-based XSS
|
|
**Confidence**: Low
|
|
**Location**: `neode-ui/public/test-aiui.html:46,50-69`
|
|
|
|
**Source file**: `neode-ui/public/test-aiui.html`
|
|
|
|
**Evidence**:
|
|
- Line 46: `results.innerHTML = ''` — uses innerHTML (safe with empty string, but establishes unsafe pattern)
|
|
- Line 50-69: Listens to all `postMessage` events with **no origin check**
|
|
- Line 107: `div.textContent = ...JSON.stringify(resp.data)...` — uses textContent (safe), but the callback at line 56 passes the full message object to arbitrary callbacks
|
|
- This is a test file deployed to production at `/test-aiui.html`
|
|
|
|
---
|
|
|
|
## XSS-007 — CORS Wildcard Enables Cross-Origin XSS Delivery
|
|
**Type**: XSS enabler (not XSS itself)
|
|
**Confidence**: High
|
|
**Location**: All backend endpoints
|
|
|
|
**Source file**: `core/archipelago/src/api/handler.rs:15` — `const CORS_ANY: &str = "*";`
|
|
|
|
**Evidence**: Every backend response includes `Access-Control-Allow-Origin: *`. This means any website can:
|
|
1. Inject stored messages via `POST /archipelago/node-message` (XSS-001)
|
|
2. Read reflected data from `echo` endpoint (XSS-005)
|
|
3. Invoke any RPC method and read responses
|
|
4. Deliver XSS payloads remotely without requiring the attacker to be on the local network
|
|
|
|
This transforms what would be LAN-only vulnerabilities into remotely exploitable ones via drive-by attacks.
|
|
|
|
---
|
|
|
|
```json
|
|
{
|
|
"category": "xss",
|
|
"findings": [
|
|
{
|
|
"id": "XSS-001",
|
|
"type": "stored_xss",
|
|
"endpoint": "/archipelago/node-message",
|
|
"parameter": "message, from_pubkey",
|
|
"confidence": "medium",
|
|
"payload_suggestion": "curl -X POST http://192.168.1.228/archipelago/node-message -H 'Content-Type: application/json' -d '{\"from_pubkey\":\"\\\" onfocus=alert(1) autofocus=\\\"\",\"message\":\"<img src=x onerror=alert(document.cookie)>\"}'"
|
|
},
|
|
{
|
|
"id": "XSS-002",
|
|
"type": "dom_xss_postmessage",
|
|
"endpoint": "AppLauncherOverlay.vue (client-side)",
|
|
"parameter": "postMessage event.data.type",
|
|
"confidence": "medium",
|
|
"payload_suggestion": "window.parent.postMessage({ type: 'app-launcher-escape' }, '*')"
|
|
},
|
|
{
|
|
"id": "XSS-003",
|
|
"type": "dom_xss_postmessage",
|
|
"endpoint": "Settings.vue (client-side)",
|
|
"parameter": "postMessage event.data.type",
|
|
"confidence": "medium",
|
|
"payload_suggestion": "window.postMessage({ type: 'claude-auth-success' }, '*')"
|
|
},
|
|
{
|
|
"id": "XSS-004",
|
|
"type": "missing_csp_headers",
|
|
"endpoint": "All responses (nginx)",
|
|
"parameter": "Content-Security-Policy, X-Frame-Options",
|
|
"confidence": "high",
|
|
"payload_suggestion": "No CSP set — any successful XSS injection has zero mitigation. Verify with: curl -sI http://192.168.1.228/ | grep -i security"
|
|
},
|
|
{
|
|
"id": "XSS-005",
|
|
"type": "reflected_xss_json",
|
|
"endpoint": "/rpc/v1 (method: echo)",
|
|
"parameter": "params.message",
|
|
"confidence": "low",
|
|
"payload_suggestion": "curl -X POST http://192.168.1.228/rpc/v1 -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":{\"message\":\"<script>alert(1)</script>\"}}'"
|
|
},
|
|
{
|
|
"id": "XSS-006",
|
|
"type": "dom_xss_postmessage",
|
|
"endpoint": "/test-aiui.html",
|
|
"parameter": "postMessage event.data",
|
|
"confidence": "low",
|
|
"payload_suggestion": "window.postMessage({ type: 'context:response', id: 'test-1', data: '<img src=x onerror=alert(1)>' }, '*')"
|
|
},
|
|
{
|
|
"id": "XSS-007",
|
|
"type": "cors_wildcard_xss_enabler",
|
|
"endpoint": "All backend endpoints",
|
|
"parameter": "Access-Control-Allow-Origin: *",
|
|
"confidence": "high",
|
|
"payload_suggestion": "From any website: fetch('http://192.168.1.228/archipelago/node-message', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({from_pubkey:'attacker', message:'<script>alert(1)</script>'})})"
|
|
}
|
|
]
|
|
}
|
|
``` |