diff --git a/get-started/csi_recv_router/main/app_main.c b/get-started/csi_recv_router/main/app_main.c index ff856d4..7240d32 100644 --- a/get-started/csi_recv_router/main/app_main.c +++ b/get-started/csi_recv_router/main/app_main.c @@ -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>:" + * Verify HMAC-signed command. Format: "HMAC:<16hex>::" + * HMAC = truncated SHA-256(secret, ":") + * 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>:: */ 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 = ":" */ + 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;