CVEReports
Reports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Reports
  • Sitemap
  • RSS Feed

Company

  • About
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Powered by Google Gemini & CVE Feed

|
•

CVE-2025-68620
CVSS 9.1|EPSS 0.17%

Signal K: Sinking the Ship with a Leaky WebSocket

Alon Barad
Alon Barad
Software Engineer•January 2, 2026•7 min read
PoC Available

Executive Summary (TL;DR)

Signal K Server broadcasted sensitive 'access request' events—including request IDs—to unauthenticated WebSocket clients. Coupled with a polling endpoint that returned plaintext JWTs upon request approval, this allowed attackers to passively snoop on legitimate login attempts and steal the resulting session tokens, granting full administrative control over the vessel's data server.

A critical authentication bypass in Signal K Server allows unauthenticated attackers to hijack administrative sessions. By listening to the public WebSocket stream for access request IDs and polling an insecure REST endpoint, attackers can steal valid JWTs the moment an administrator approves a legitimate device.

The Hook: The Internet of Floating Things

Modern sailing isn't just about reading the wind and hoisting sails; it's about JSON, WebSockets, and Node.js. Enter Signal K, the open-source standard for marine data exchange. It's the central nervous system of a smart boat, aggregating data from NMEA 2000 networks—GPS, depth, engine diagnostics, AIS targets—and serving it up via REST and WebSockets to iPads, plotters, and cloud dashboards.

Ideally, this server sits safely behind a firewall, accessible only to the captain. But in reality? It's often exposed to the marina Wi-Fi or the open internet for remote monitoring. And that is where things get interesting.

CVE-2025-68620 isn't a complex memory corruption bug requiring heap feng shui. It’s a logic flaw born from the classic developer desire to make things "reactive." The developers wanted the UI to update instantly when a device requested access. Their solution? Broadcast everything. Unfortunately, in doing so, they inadvertently created a public address system for the ship's most sensitive secrets.

The Flaw: Loose Lips Sink Ships

To understand this vulnerability, you have to look at how Signal K handles device authorization. When a new device (say, the Captain's iPad) wants to talk to the server, it sends an access request. This request sits in a pending queue until an administrator approves it via the dashboard.

The flaw lies in the intersection of two features designed for convenience:

  1. The Town Crier (WebSocket Leak): The server maintains a WebSocket endpoint at /signalk/v1/stream?serverevents=all. As the query parameter implies, this stream emits server events. The fatal mistake? It emitted ACCESS_REQUEST events to unauthenticated clients. These events contained the requestId, the device name, and the permissions requested.

  2. The Open Vault (Polling Leak): The server also provided a REST endpoint (/signalk/v1/access/requests/:id) so a device could poll the status of its request. Logically, you’d expect this endpoint to require some proof of identity. Instead, if the server was configured with allow_readonly (a common default), anyone could query the status of any request ID.

Here is the kicker: When an admin approves a request, the server updates the state of that request ID. The next time the polling endpoint is hit, it returns a JSON object containing the status COMPLETED and, crucially, the newly minted access token (JWT) in plaintext. It’s like a bank telling everyone in the lobby the combination to the vault every time a customer makes a deposit.

The Code: Anatomy of a Leak

Let's look at the logic that doomed the ship. In the pre-patch versions, the event handling logic didn't discriminate between who was listening. If you connected to the stream, you got the firehose.

Here is a simplified view of the vulnerable logic in src/events.ts:

// VULNERABLE CODE (Conceptual)
app.on('serverEvent', (event) => {
  // Broadcast to EVERYONE connected to ?serverevents=all
  connectedClients.forEach(client => {
    client.send(JSON.stringify(event));
  });
});

Because ACCESS_REQUEST was categorized as a generic server event, the payload leaked immediately. The payload looked something like this:

{
  "type": "ACCESS_REQUEST",
  "data": {
    "requestId": "c0ffee-1234-5678",
    "remoteAddress": "::ffff:192.168.1.50",
    "clientId": "Captains-iPad"
  }
}

Armed with the requestId (c0ffee-1234-5678), an attacker simply issues a GET request:

GET /signalk/v1/access/requests/c0ffee-1234-5678

Before approval, it returns PENDING. The moment the admin clicks "Approve", the response changes to:

{
  "state": "COMPLETED",
  "accessRequest": {
    "permission": "admin",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI...[FULL_ADMIN_JWT]"
  }
}

The fix, introduced in commit 221aff6, bifurcated the event stream. They introduced a serverAdminEvent category. Now, the public stream only gets benign noise, while the sensitive events are gated behind an hasAdminAccess check.

The Exploit: Mutiny on the Bounty

A savvy attacker can exploit this in two ways: passive patience or active deception. Let's walk through the Active Deception path, which chains this CVE with CVE-2025-69203 (IP Spoofing) for maximum impact.

Step 1: The Set-Up

The attacker connects to the WebSocket stream: ws://target-boat.local/signalk/v1/stream?serverevents=all

Step 2: The Bait

The attacker sends an access request. To lower the admin's suspicion, they spoof their IP address to look like localhost (127.0.0.1) using the X-Forwarded-For header (CVE-2025-69203). They also choose a boring, trustworthy name.

curl -X POST http://target-boat.local/signalk/v1/access/requests \
  -H "X-Forwarded-For: 127.0.0.1" \
  -d '{"clientId": "System-Diagnostic-Service", "description": "Internal Watchdog"}'

Step 3: The Hook

The WebSocket immediately broadcasts the new requestId back to the attacker (and everyone else). The attacker captures this ID.

Step 4: The Catch

The admin logs in and sees a request from "System-Diagnostic-Service" originating from 127.0.0.1. Thinking it's an internal system process needing a reconnect, they click Approve.

Step 5: The Prize

The attacker, who has been polling the status endpoint every second, instantly receives the JWT. They now have full administrative control over the Signal K server.

The Impact: Dead in the Water

Why does this matter? It's just boat data, right? Wrong. Signal K isn't just a passive display; it's often integrated with NMEA 2000 writing capabilities. An attacker with an admin token can do far more than just see where the boat is located.

1. Data Exfiltration: Access to the full history of the vessel's movements, depth logs, and engine usage. For high-value targets (superyachts, commercial vessels), this is sensitive intelligence.

2. Configuration Tampering: An attacker can alter alarm thresholds. Imagine disabling the depth alarm or the engine temperature warnings while the boat is navigating tricky waters.

3. Remote Code Execution (RCE): Signal K runs on Node.js and supports plugins. An admin can often install plugins or modify server configuration in ways that allow for arbitrary code execution on the host operating system (often a Raspberry Pi or a localized server). From there, the attacker can pivot to other devices on the vessel's network, potentially interfering with navigation systems.

This is a full compromise of the vessel's digital bridge.

The Mitigation: Patching the Hull

If you are running Signal K, you are likely technically inclined. Here is what you need to do immediately.

1. Update: Upgrade to Signal K Server v2.19.0 or later. The developers have patched the leak by introducing serverAdminEvent and restricting who can subscribe to it. This kills the WebSocket leak vector.

2. Defense in Depth: Review your settings.json. If you have allow_readonly set to true, ask yourself if you really need unauthenticated users polling your API. Setting this to false creates an additional barrier, requiring authentication before anyone can even check a request status.

3. Trust No One: Be extremely skeptical of access requests appearing in your dashboard, even if they claim to be from localhost or a known device. If you didn't trigger the request yourself, deny it.

[!NOTE] The fix works by ensuring that sendAccessRequestsUpdate only emits to the restricted channel. If you are auditing your own code, look for any event emitters that broadcast user-input data to global channels.

Official Patches

Signal KSignal K Server v2.19.0 Release Notes

Fix Analysis (1)

Technical Appendix

CVSS Score
9.1/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
EPSS Probability
0.17%
Top 100% most exploited
2,500
Estimated exposed hosts via Shodan

Affected Systems

Signal K Server

Affected Versions Detail

ProductAffected VersionsFixed Version
Signal K Server
Signal K
< 2.19.02.19.0
AttributeDetail
Attack VectorNetwork (WebSocket & REST)
CVSS v3.19.1 (Critical)
CWECWE-306 (Missing Authentication for Critical Function)
Exploit StatusPoC Available
ImpactFull Admin Compromise
PrerequisitesNone (Unauthenticated)

MITRE ATT&CK Mapping

MITRE ATT&CK Mapping

T1078.003Valid Accounts: Local Accounts
Initial Access
T1589.002Gather Victim Identity Information
Reconnaissance
T1036.007Masquerading: IP Spoofing
Defense Evasion
CWE-306
Missing Authentication for Critical Function

The software does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.

Exploit Resources

Known Exploits & Detection

Research AnalysisLogic flaw in event handling allows token recovery.

Vulnerability Timeline

Vulnerability Timeline

Patch Committed (v2.19.0)
2025-01-20
CVE Published
2025-02-14

References & Sources

  • [1]Signal K Server Repository
Related Vulnerabilities
CVE-2025-69203

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.