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>
11 KiB
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:
- The toast at
App.vue:52renders the raw message text as a notification preview — if Vue's escaping were ever bypassed (e.g., a future refactor introducesv-html), this becomes immediately exploitable - The
:titleattribute binding atWeb5.vue:402(<p ... :title="m.from_pubkey">) accepts the unsanitized pubkey — attribute injection is possible with crafted pubkey values - 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:
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:
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:
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:
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:
// 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:
- No CSP header set on any response — no defense-in-depth against XSS
- No X-Frame-Options — clickjacking possible on main UI
- No X-Content-Type-Options — MIME sniffing attacks possible
- 25+ app proxy locations explicitly strip CSP and X-Frame-Options:
This removes the security headers that apps like Vaultwarden, Portainer, and Grafana set to protect themselves, making them vulnerable to clickjacking when proxied.proxy_hide_header X-Frame-Options; proxy_hide_header Content-Security-Policy;
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:
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:
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
postMessageevents 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:
- Inject stored messages via
POST /archipelago/node-message(XSS-001) - Read reflected data from
echoendpoint (XSS-005) - Invoke any RPC method and read responses
- 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.
{
"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>'})})"
}
]
}