fix: Harden HMAC auth, sanitize inputs, throttle NVS writes

- Constant-time HMAC comparison (prevents timing side-channel)
- Add timestamp to HMAC scheme for replay protection (30s window)
  New format: HMAC:<16hex>:<uptime_s>:<cmd>
- Validate HOSTNAME against [a-z0-9-] to prevent UDP stream injection
- Sanitize probe request SSIDs (strip non-printable chars and commas)
- Redact HMAC token from serial log output
- NVS write throttle: max 20 writes per 10s to prevent flash wear
This commit is contained in:
user
2026-02-14 18:41:21 +01:00
parent ebc8a00b46
commit 476a9beb3b

View File

@@ -368,8 +368,31 @@ static void config_load_nvs(void)
}
}
/* NVS write throttle: max 20 writes per 10s to prevent flash wear attacks */
#define NVS_WRITES_MAX 20
#define NVS_WINDOW_US 10000000LL
static int64_t s_nvs_window_start = 0;
static int s_nvs_window_writes = 0;
static bool nvs_write_throttle(void)
{
int64_t now = esp_timer_get_time();
if (now - s_nvs_window_start > NVS_WINDOW_US) {
s_nvs_window_start = now;
s_nvs_window_writes = 0;
}
if (s_nvs_window_writes >= NVS_WRITES_MAX) {
ESP_LOGW(TAG, "NVS write throttled");
return false;
}
s_nvs_window_writes++;
return true;
}
static esp_err_t config_save_i32(const char *key, int32_t value)
{
if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
@@ -381,6 +404,7 @@ static esp_err_t config_save_i32(const char *key, int32_t value)
static esp_err_t config_save_i8(const char *key, int8_t value)
{
if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
@@ -392,6 +416,7 @@ static esp_err_t config_save_i8(const char *key, int8_t value)
static esp_err_t config_save_str(const char *key, const char *value)
{
if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
@@ -403,6 +428,7 @@ static esp_err_t config_save_str(const char *key, const char *value)
static esp_err_t config_save_blob(const char *key, const void *data, size_t len)
{
if (!nvs_write_throttle()) return ESP_ERR_INVALID_STATE;
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
@@ -1364,6 +1390,12 @@ static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
if (ssid_len > 0 && ssid_len <= 32 && ssid_len + 2 <= body_len) {
memcpy(ssid, &body[2], ssid_len);
ssid[ssid_len] = '\0';
/* Sanitize: replace non-printable and CSV-breaking chars */
for (int j = 0; j < ssid_len; j++) {
if (ssid[j] < 0x20 || ssid[j] > 0x7e || ssid[j] == ',') {
ssid[j] = '?';
}
}
}
}
@@ -1398,7 +1430,9 @@ static void wifi_promiscuous_init(void)
/* --- HMAC command authentication --- */
/**
* Verify HMAC-signed command. Format: "HMAC:<16hex>:<cmd>"
* Verify HMAC-signed command. Format: "HMAC:<16hex>:<uptime_s>:<cmd>"
* HMAC = truncated SHA-256(secret, "<uptime_s>:<cmd>")
* Timestamp must be within 30s of device uptime (replay protection).
* Returns pointer to actual command on success, or NULL on failure
* (with error message written to reply).
*/
@@ -1415,7 +1449,7 @@ static const char *auth_verify(const char *input, char *reply, size_t reply_size
return NULL;
}
/* Find second colon after 16 hex chars */
/* Parse: HMAC:<16 hex chars>:<uptime_s>:<cmd> */
if (strlen(input) < 5 + 16 + 1) {
snprintf(reply, reply_size, "ERR AUTH malformed");
return NULL;
@@ -1425,16 +1459,33 @@ static const char *auth_verify(const char *input, char *reply, size_t reply_size
return NULL;
}
const char *cmd = input + 5 + 16 + 1;
/* payload = "<uptime_s>:<cmd>" */
const char *payload = input + 5 + 16 + 1;
const char *cmd_sep = strchr(payload, ':');
if (!cmd_sep) {
snprintf(reply, reply_size, "ERR AUTH malformed (need timestamp:cmd)");
return NULL;
}
/* Compute HMAC-SHA256 of the command */
/* Replay protection: reject stale timestamps */
long ts = strtol(payload, NULL, 10);
int64_t now_s = esp_timer_get_time() / 1000000LL;
int64_t drift = now_s - (int64_t)ts;
if (drift < -30 || drift > 30) {
snprintf(reply, reply_size, "ERR AUTH expired (drift=%llds)", (long long)drift);
return NULL;
}
const char *cmd = cmd_sep + 1;
/* Compute HMAC-SHA256 over payload (timestamp:cmd) */
uint8_t hmac[32];
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_setup(&ctx, md_info, 1);
mbedtls_md_hmac_starts(&ctx, (const uint8_t *)s_auth_secret, strlen(s_auth_secret));
mbedtls_md_hmac_update(&ctx, (const uint8_t *)cmd, strlen(cmd));
mbedtls_md_hmac_update(&ctx, (const uint8_t *)payload, strlen(payload));
mbedtls_md_hmac_finish(&ctx, hmac);
mbedtls_md_free(&ctx);
@@ -1444,7 +1495,12 @@ static const char *auth_verify(const char *input, char *reply, size_t reply_size
snprintf(expected + i * 2, 3, "%02x", hmac[i]);
}
if (strncmp(input + 5, expected, 16) != 0) {
/* Constant-time comparison (prevents timing side-channel) */
volatile uint8_t diff = 0;
for (int i = 0; i < 16; i++) {
diff |= (uint8_t)input[5 + i] ^ (uint8_t)expected[i];
}
if (diff != 0) {
snprintf(reply, reply_size, "ERR AUTH failed");
return NULL;
}
@@ -1794,6 +1850,14 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
snprintf(reply, reply_size, "ERR HOSTNAME length 1-%d", (int)sizeof(s_hostname) - 1);
return strlen(reply);
}
/* Validate: lowercase alphanumeric and hyphens only */
for (size_t i = 0; i < nlen; i++) {
char c = name[i];
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')) {
snprintf(reply, reply_size, "ERR HOSTNAME chars: a-z 0-9 -");
return strlen(reply);
}
}
strncpy(s_hostname, name, sizeof(s_hostname) - 1);
s_hostname[sizeof(s_hostname) - 1] = '\0';
config_save_str("hostname", s_hostname);
@@ -2469,7 +2533,12 @@ static void cmd_task(void *arg)
}
rx_buf[len] = '\0';
ESP_LOGI(TAG, "CMD rx: \"%s\"", rx_buf);
/* Log command (redact HMAC token) */
if (strncmp(rx_buf, "HMAC:", 5) == 0 && strlen(rx_buf) > 22) {
ESP_LOGI(TAG, "CMD rx: HMAC:****:%s", rx_buf + 22);
} else {
ESP_LOGI(TAG, "CMD rx: \"%s\"", rx_buf);
}
/* Authenticate: HMAC grants full access; plain commands are read-only */
const char *cmd = rx_buf;