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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user