# Pentest Results — ESP32 CSI Sensor Firmware

**Date:** 2026-02-14
**Target:** amber-maple (192.168.129.30), ESP32-D0WD-V3 rev 3.1
**Firmware:** v1.11.0-11-ga4bd2a6-dirty (v1.12-dev security hardening)
**ESP-IDF:** v5.5.2
**Tester:** Raspberry Pi 5 (192.168.129.11), same LAN

---

## Results Matrix

### Phase 1: Passive Reconnaissance

| # | Test | Status | Notes |
|---|------|--------|-------|
| 1a | mDNS service discovery | **PASS** | No `_esp-csi._udp` service advertised. Only hostname.local resolves. |
| 1b | Port scan (UDP) | **PASS** | Only 5353/udp (mDNS) and 5501/udp (cmd) open. 5500/udp closed (outbound-only). |
| 1b | Port scan (TCP) | **PASS** | All 1000 TCP ports closed. Zero TCP attack surface. |
| 1c | Firmware strings | **PASS** | No hardcoded secrets, passwords, or embedded private keys. |
| 1c | Firmware strings | **WARN** | Hardcoded default IP `192.168.129.11` and hostname `amber-maple` in binary. |
| 1c | Entropy analysis | **INFO** | Firmware not flash-encrypted (low entropy). Expected for dev boards. |
| 1c | PEM markers | **INFO** | mbedTLS parser constants only, not actual embedded keys. |
| 1d | eFuse: JTAG | **WARN** | `JTAG_DISABLE = False` — JTAG debug accessible via GPIO 12-15. |
| 1d | eFuse: Secure Boot | **WARN** | `ABS_DONE_0 = False`, `ABS_DONE_1 = False` — no secure boot. |
| 1d | eFuse: Flash Encryption | **WARN** | `FLASH_CRYPT_CNT = 0` — flash encryption disabled. |
| 1d | eFuse: Download Mode | **WARN** | `DISABLE_DL_ENCRYPT/DECRYPT/CACHE = False` — UART download mode open. |
| 1d | eFuse: Coding Scheme | **INFO** | `CODING_SCHEME = NONE` — full 256-bit key space available. |

### Phase 2: Network Protocol Analysis

| # | Test | Status | Notes |
|---|------|--------|-------|
| 2a | Unauth STATUS info leak | **PASS** | Minimal response (hostname, uptime, rssi, channel, version). No secrets. |
| 2a | CONFIG info disclosure | **PASS** | Auth secret not exposed in unauthed CONFIG response. |
| 2a | HMAC on wire | **INFO** | HMAC tags visible in plaintext UDP (expected — no DTLS). |
| 2a | HELP disclosure | **INFO** | Command list visible without auth (by design). |
| 2b | HMAC timing oracle | **PASS** | Median diff 144us between test cases — within WiFi jitter. Constant-time comparison effective. |
| 2c | Null byte injection | **PASS** | All 17 unauthed injection payloads handled safely. |
| 2c | Format string injection | **PASS** | `%x%x%n` payloads rejected; no stack leak. |
| 2c | Newline/CRLF injection | **PASS** | Rejected by auth requirement. |
| 2c | Oversized packets | **PASS** | MTU-sized and 191-byte packets rejected gracefully. |
| 2c | Binary garbage | **PASS** | Random byte payloads handled without crash. |
| 2c | HOSTNAME format string | **PASS** | Authed: format strings rejected (timeout, likely sanitized). |
| 2c | HOSTNAME oversized | **PASS** | `ERR HOSTNAME length 1-31` — proper validation. |
| 2c | HOSTNAME bad chars | **PASS** | `ERR HOSTNAME chars: a-z 0-9 -` — strict allowlist. |
| 2c | TARGET format string | **PASS** | `ERR TARGET invalid IP` — proper validation. |
| 2c | RATE boundary values | **PASS** | All out-of-range values rejected: `ERR RATE range 10-100`. |
| 2d | Valid HMAC command | **PASS** | Authed STATUS returns full response. |
| 2d | Immediate replay | **PASS** | `ERR AUTH replay rejected` — nonce dedup works. |
| 2d | Expired timestamp (-10s) | **PASS** | `ERR AUTH expired (drift=10s)` — window enforced. |
| 2d | Future timestamp (+10s) | **PASS** | `ERR AUTH expired (drift=-10s)` — window enforced. |
| 2d | Wrong secret | **PASS** | `ERR AUTH failed` — incorrect HMAC rejected. |
| 2d | Nonce cache overflow (9 cmds) | **PASS** | Replay still rejected after flooding cache with 9 unique commands. |

### Phase 3: Static Analysis

| # | Test | Status | Notes |
|---|------|--------|-------|
| 3a | NVS auth_secret storage | **FAIL** | Stored as plaintext string: `86bd963b07858d5b10db839d55b409df` at offset 0x1ba8. |
| 3a | NVS WiFi credentials | **PASS** | No WiFi SSID/password found in NVS dump (compiled-in via sdkconfig). |
| 3a | NVS integrity | **WARN** | 2 of 6 pages have invalid CRC (uninitialized, not corruption). |
| 3a | Boot history | **INFO** | `boot_count = 25` visible — leaks reboot frequency. |
| 3b | CVE exposure (12 checked) | **PASS** | 8 CVEs not applicable (unused components). 4 LOW risk (BLE scan-only mitigates). |
| 3b | CVE-2025-27840 (HCI cmds) | **LOW** | Hidden HCI commands; mitigated by scan-only BLE mode. |
| 3b | Unused `#include "esp_now.h"` | **INFO** | Dead include — remove to avoid link to CVE-2025-52471. |
| 3c | Stack canaries | **FAIL** | `CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y` — no stack overflow protection. |
| 3c | Heap poisoning | **WARN** | `CONFIG_HEAP_POISONING_DISABLED=y` — no heap corruption detection. |
| 3c | PMF configuration | **PASS** | `CONFIG_ESP_WIFI_PMF_REQUIRED=y` — 802.11w enforced. |
| 3c | Debug symbols in ELF | **INFO** | ELF has full debug info (11.9 MB). `.bin` for OTA is stripped. |
| 3c | cmd_task size | **INFO** | 9,877 bytes compiled — large function handling untrusted input. |
| 3c | Watchdog | **PASS** | Bootloader, interrupt, and task WDTs all enabled. |

---

## Risk Assessment

### Critical (requires physical access)

| Risk | Impact | Mitigation |
|------|--------|------------|
| No flash encryption | Auth secret + WiFi creds readable from flash | Enable `CONFIG_FLASH_ENCRYPTION_ENABLED` (irreversible eFuse) |
| No secure boot | Arbitrary firmware flashable via UART/OTA | Enable Secure Boot V2 (irreversible eFuse) |
| JTAG enabled | Live memory inspection, breakpoints | Burn `JTAG_DISABLE` eFuse (irreversible) |
| NVS plaintext | `auth_secret` in cleartext NVS | Flash encryption covers this |

### Medium (network-accessible)

| Risk | Impact | Mitigation |
|------|--------|------------|
| No DTLS | HMAC tokens visible to LAN sniffers | Implement DTLS for command channel |
| No stack canaries | Buffer overflow in cmd_task could be exploitable | Enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM` |
| OTA over HTTP | MITM firmware injection on LAN | Embed CA cert, enforce HTTPS OTA |

### Low / Informational

| Risk | Impact | Mitigation |
|------|--------|------------|
| Hardcoded default IP | Network topology leak in binary | Move to Kconfig or NVS-only default |
| Version string leaks git hash | Aids targeted attacks | Use clean tag-only version strings |
| HELP visible without auth | Command enumeration | By design — acceptable |
| Uptime in unauthed STATUS | Aids HMAC timestamp prediction | Already in minimal STATUS by design |

---

## Security Hardening Scorecard

| Category | Score | Notes |
|----------|-------|-------|
| **Authentication** | 9/10 | HMAC-SHA256, replay protection, nonce cache, rate limiting. Only gap: no mutual auth. |
| **Input Validation** | 10/10 | All 27 injection tests passed. Strict allowlists on HOSTNAME, RATE, TARGET, POWER. |
| **Network Exposure** | 8/10 | Minimal ports, PMF required, service ads removed. Gap: plaintext UDP. |
| **Physical Security** | 2/10 | No secure boot, no flash encryption, JTAG open, UART download mode open. |
| **Binary Security** | 4/10 | No stack canaries, no heap poisoning. WDTs present. |
| **CVE Exposure** | 9/10 | Minimal attack surface; unused components disabled; v5.5.2 patches applied. |

**Overall: Strong network security, weak physical security.**
The firmware is well-hardened against remote/network attacks. Physical access remains the primary threat vector.

---

## Recommendations (Priority Order)

1. **P1** — Enable stack canaries: `CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y`
2. **P1** — Enable heap poisoning: `CONFIG_HEAP_POISONING_LIGHT=y`
3. **P2** — Enable WDT panic: `CONFIG_ESP_TASK_WDT_PANIC=y`
4. **P2** — Remove unused `#include "esp_now.h"`
5. **P2** — Remove hardcoded default IP from binary
6. **P3** — Flash encryption (requires eFuse planning)
7. **P3** — Secure Boot V2 (requires eFuse planning)
8. **P3** — DTLS for command channel (significant effort)
9. **P3** — OTA certificate pinning
