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.
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.
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:
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.
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.
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.
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.
The attacker connects to the WebSocket stream:
ws://target-boat.local/signalk/v1/stream?serverevents=all
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"}'The WebSocket immediately broadcasts the new requestId back to the attacker (and everyone else). The attacker captures this ID.
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.
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.
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.
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
sendAccessRequestsUpdateonly 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.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Signal K Server Signal K | < 2.19.0 | 2.19.0 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network (WebSocket & REST) |
| CVSS v3.1 | 9.1 (Critical) |
| CWE | CWE-306 (Missing Authentication for Critical Function) |
| Exploit Status | PoC Available |
| Impact | Full Admin Compromise |
| Prerequisites | None (Unauthenticated) |
The software does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.
Get the latest CVE analysis reports delivered to your inbox.