Skip to content

Commit 0b67806

Browse files
1technophileclaude
andauthored
[SYS] Bound remaining MQTT/HTTP/BLE/RF inputs into fixed buffers (#2324)
Follow-up to #2321. That PR fixed five strcpy sites in XtoSYS/createOrUpdateDevice but left adjacent attack surfaces using the same pattern (strncpy with the destination's own size, leaving the buffer non-NUL-terminated when the source is at the limit; or strcpy of WiFiManager/HTTP form values that bypass the client-side maxlength). The over-long string then flows into saveConfig() and back into a strcpy on next boot, corrupting heap/.bss on every reload. main/main.cpp - XtoSYS: length-check before copying mqtt_topic, discovery_prefix, gateway_name, gw_pass (ota_pass), and ble_aes. Reject over-length with THEENGS_LOG_WARNING, preserving existing values. main/main.cpp - loadConfigFromFlash: new loadStrField() helper applies the same guard to every SPIFFS-loaded string (mqtt_server/port/user/pass per slot, mqtt_topic, discovery_prefix, gateway_name, ota_pass, ble_aes). Defense in depth in case a pre-fix build left an over-length value in /config.json. main/main.cpp - WiFiManager save: bound mqtt_topic against the 150-byte portal buffer overflowing the 66-byte destination (HTML maxlength is client-side only; a raw POST bypasses it). main/webUI.cpp - handleCG /gw save: bound gateway-password form input before the strncpy that previously left ota_pass non-NUL-terminated. main/blufi.cpp - ESP_BLUFI_EVENT_RECV_STA_SSID/PASSWD: clamp the wire-supplied ssid_len/passwd_len before the memcpy and the trailing NUL so a crafted BluFi provisioning frame cannot overrun gl_sta_ssid[32]/gl_sta_passwd[64]. main/gatewayBT.cpp - BLEDecryptor (Victron, encr==3): remove `nonce[16] = {0}`, a one-byte stack-OOB write reachable from any BLE adv. The line was dead (the memset two lines later zeroes the whole array); deleting it eliminates the OOB write. main/gatewaySRFB.cpp - SRFBtoX: bound the _uartbuf writer so a 433 MHz frame with no STOP can no longer let _uartpos grow past sizeof(_uartbuf). main/gatewaySRFB.cpp - _rfbSend: the hex output buffer was sized RF_MESSAGE_SIZE (9) but _rawToHex writes 2*N+2 bytes per call. Resize to fit. Tested on a Theengs Bridge: built theengs-bridge-v11 env, OTA-flashed, and verified that publishing {"discovery_prefix":"Z"*200} to MQTTtoSYS/config leaves discovery_prefix unchanged (no Z*65 truncation in subsequent discovery topics), bridge keeps decoding BLE adverts, and SYStoMQTT/LWT stay healthy. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2459e1c commit 0b67806

5 files changed

Lines changed: 113 additions & 29 deletions

File tree

main/blufi.cpp

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,16 +321,27 @@ static void example_event_callback(esp_blufi_cb_event_t event, esp_blufi_cb_para
321321
THEENGS_LOG_TRACE(F("BLUFI recv slave disconnect a ble connection" CR));
322322
esp_blufi_disconnect();
323323
break;
324-
case ESP_BLUFI_EVENT_RECV_STA_SSID:
325-
strncpy((char*)gl_sta_ssid, (char*)param->sta_ssid.ssid, param->sta_ssid.ssid_len);
326-
gl_sta_ssid[param->sta_ssid.ssid_len] = '\0';
324+
case ESP_BLUFI_EVENT_RECV_STA_SSID: {
325+
// ssid_len comes from an unauthenticated BLE provisioning frame; clamp it
326+
// before the strncpy and the trailing NUL so a crafted long SSID cannot
327+
// write past gl_sta_ssid.
328+
size_t ssid_len = param->sta_ssid.ssid_len;
329+
if (ssid_len > sizeof(gl_sta_ssid) - 1)
330+
ssid_len = sizeof(gl_sta_ssid) - 1;
331+
memcpy(gl_sta_ssid, param->sta_ssid.ssid, ssid_len);
332+
gl_sta_ssid[ssid_len] = '\0';
327333
THEENGS_LOG_NOTICE(F("Recv STA SSID %s" CR), gl_sta_ssid);
328334
break;
329-
case ESP_BLUFI_EVENT_RECV_STA_PASSWD:
330-
strncpy((char*)gl_sta_passwd, (char*)param->sta_passwd.passwd, param->sta_passwd.passwd_len);
331-
gl_sta_passwd[param->sta_passwd.passwd_len] = '\0';
335+
}
336+
case ESP_BLUFI_EVENT_RECV_STA_PASSWD: {
337+
size_t passwd_len = param->sta_passwd.passwd_len;
338+
if (passwd_len > sizeof(gl_sta_passwd) - 1)
339+
passwd_len = sizeof(gl_sta_passwd) - 1;
340+
memcpy(gl_sta_passwd, param->sta_passwd.passwd, passwd_len);
341+
gl_sta_passwd[passwd_len] = '\0';
332342
THEENGS_LOG_NOTICE(F("Recv STA PASSWORD" CR));
333343
break;
344+
}
334345
case ESP_BLUFI_EVENT_GET_WIFI_LIST: {
335346
WiFi.scanNetworks(true);
336347
break;

main/gatewayBT.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,7 +1348,10 @@ void process_bledata(JsonObject& BLEdata) {
13481348
THEENGS_LOG_TRACE(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str());
13491349

13501350
} else if (BLEdata["encr"].as<int>() == 3) {
1351-
nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7
1351+
// Victron has a 16-byte zero-padded nonce with IV bytes 0,1.
1352+
// (The line `nonce[16] = {0};` that lived here wrote a byte one past the
1353+
// end of the 16-byte `nonce` array on every Victron-encrypted advert;
1354+
// memset below already zeroes the whole buffer, so the typo was dead.)
13521355
unsigned char iv[2];
13531356
int ivlen = hexToBytes(BLEdata["ctr"].as<String>(), iv, 2);
13541357
if (ivlen != 2) {

main/gatewaySRFB.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ void _rfbSend(byte* message) {
6060
}
6161

6262
void _rfbSend(byte* message, int times) {
63-
char buffer[RF_MESSAGE_SIZE];
63+
// _rawToHex writes 3 bytes per input byte (two hex chars + CR) followed by a
64+
// terminating NUL, so the buffer needs 2 * RF_MESSAGE_SIZE + 2 bytes.
65+
char buffer[RF_MESSAGE_SIZE * 2 + 2] = {0};
6466
TheengsUtils::_rawToHex(message, buffer, RF_MESSAGE_SIZE);
6567
THEENGS_LOG_NOTICE(F("[RFBRIDGE] Sending MESSAGE" CR));
6668

@@ -85,8 +87,15 @@ bool SRFBtoX() {
8587
if (c == RF_CODE_STOP) {
8688
_rfbDecode();
8789
receiving = false;
88-
} else {
90+
} else if (_uartpos < sizeof(_uartbuf)) {
8991
_uartbuf[_uartpos++] = c;
92+
} else {
93+
// Frame longer than expected — drop it rather than overrun _uartbuf.
94+
// A crafted 433 MHz transmission that emits START but never STOP would
95+
// otherwise let _uartpos grow up to 255 and trample adjacent globals.
96+
THEENGS_LOG_WARNING(F("[RFBRIDGE] RF frame too long, dropping" CR));
97+
receiving = false;
98+
_uartpos = 0;
9099
}
91100
} else if (c == RF_CODE_START) {
92101
_uartpos = 0;

main/main.cpp

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,19 @@ void saveConfig() {
20492049
configFile.close();
20502050
}
20512051

2052+
// Length-checked copy used when loading config.json values into fixed-size
2053+
// buffers. The SPIFFS file is implicitly trusted once written, but historical
2054+
// MQTT/HTTP paths could land over-length values there before we hardened them;
2055+
// this prevents one stale config from overflowing globals on every boot.
2056+
static void loadStrField(const char* name, char* dst, size_t dstSize, const char* src) {
2057+
if (!src) return;
2058+
if (strlen(src) >= dstSize) {
2059+
THEENGS_LOG_WARNING(F("Config field %s too long (%u), skipping" CR), name, (unsigned)strlen(src));
2060+
return;
2061+
}
2062+
strcpy(dst, src);
2063+
}
2064+
20522065
bool loadConfigFromFlash() {
20532066
THEENGS_LOG_TRACE(F("mounting FS..." CR));
20542067
bool result = false;
@@ -2124,22 +2137,22 @@ bool loadConfigFromFlash() {
21242137
strcpy(key, "mqtt_server");
21252138
strcat(key, index_suffix);
21262139
if (json.containsKey(key)) {
2127-
strcpy(cnt_parameters_array[i].mqtt_server, json[key].as<const char*>());
2140+
loadStrField(key, cnt_parameters_array[i].mqtt_server, sizeof(cnt_parameters_array[i].mqtt_server), json[key].as<const char*>());
21282141
}
21292142
strcpy(key, "mqtt_port");
21302143
strcat(key, index_suffix);
21312144
if (json.containsKey(key)) {
2132-
strcpy(cnt_parameters_array[i].mqtt_port, json[key].as<const char*>());
2145+
loadStrField(key, cnt_parameters_array[i].mqtt_port, sizeof(cnt_parameters_array[i].mqtt_port), json[key].as<const char*>());
21332146
}
21342147
strcpy(key, "mqtt_user");
21352148
strcat(key, index_suffix);
21362149
if (json.containsKey(key)) {
2137-
strcpy(cnt_parameters_array[i].mqtt_user, json[key].as<const char*>());
2150+
loadStrField(key, cnt_parameters_array[i].mqtt_user, sizeof(cnt_parameters_array[i].mqtt_user), json[key].as<const char*>());
21382151
}
21392152
strcpy(key, "mqtt_pass");
21402153
strcat(key, index_suffix);
21412154
if (json.containsKey(key)) {
2142-
strcpy(cnt_parameters_array[i].mqtt_pass, json[key].as<const char*>());
2155+
loadStrField(key, cnt_parameters_array[i].mqtt_pass, sizeof(cnt_parameters_array[i].mqtt_pass), json[key].as<const char*>());
21432156
}
21442157
strcpy(key, "mqtt_broker_secure");
21452158
strcat(key, index_suffix);
@@ -2166,15 +2179,16 @@ bool loadConfigFromFlash() {
21662179
}
21672180
# endif
21682181
if (json.containsKey("mqtt_topic"))
2169-
strcpy(mqtt_topic, json["mqtt_topic"]);
2182+
loadStrField("mqtt_topic", mqtt_topic, sizeof(mqtt_topic), json["mqtt_topic"]);
21702183
# ifdef ZmqttDiscovery
21712184
if (json.containsKey("discovery_prefix"))
2172-
strcpy(discovery_prefix, json["discovery_prefix"]);
2185+
// discovery_prefix is extern char[] in this TU; size is parameters_size + 1.
2186+
loadStrField("discovery_prefix", discovery_prefix, parameters_size + 1, json["discovery_prefix"]);
21732187
# endif
21742188
if (json.containsKey("gateway_name"))
2175-
strcpy(gateway_name, json["gateway_name"]);
2189+
loadStrField("gateway_name", gateway_name, sizeof(gateway_name), json["gateway_name"]);
21762190
if (json.containsKey("ota_pass")) {
2177-
strcpy(ota_pass, json["ota_pass"]);
2191+
loadStrField("ota_pass", ota_pass, sizeof(ota_pass), json["ota_pass"]);
21782192
# ifdef WM_PWD_FROM_MAC // From ESP Mac Address, last 8 digits as the password
21792193
// Compare the existing ota_pass if ota_pass = OTAPASSWORD then replace with the last 8 digits of the mac address
21802194
// This enable user migrating from previous version to have the same WiFi portal password as previously unless they changed it
@@ -2187,7 +2201,7 @@ bool loadConfigFromFlash() {
21872201
}
21882202
# if BLEDecryptor
21892203
if (json.containsKey("ble_aes")) {
2190-
strcpy(ble_aes, json["ble_aes"]);
2204+
loadStrField("ble_aes", ble_aes, sizeof(ble_aes), json["ble_aes"]);
21912205
THEENGS_LOG_TRACE(F("loaded default BLE AES key %s" CR), ble_aes);
21922206
}
21932207
if (json.containsKey("ble_aes_keys")) {
@@ -2376,9 +2390,17 @@ void setupWiFiManager() {
23762390
strcpy(cnt_parameters_array[cnt_index].mqtt_pass, custom_mqtt_pass.getValue());
23772391
}
23782392
if (strlen(custom_mqtt_topic.getValue()) > 0) {
2379-
strcpy(mqtt_topic, custom_mqtt_topic.getValue());
2380-
if (mqtt_topic[strlen(mqtt_topic) - 1] != '/' && strlen(mqtt_topic) < parameters_size) {
2381-
strcat(mqtt_topic, "/");
2393+
// The WiFiManagerParameter buffer is mqtt_topic_max_size (150) but the
2394+
// destination is sizeof(mqtt_topic). HTML maxlength is client-side only;
2395+
// a raw POST can deliver any length, so length-check before copying.
2396+
const char* src = custom_mqtt_topic.getValue();
2397+
if (strlen(src) >= sizeof(mqtt_topic)) {
2398+
THEENGS_LOG_WARNING(F("mqtt_topic from portal too long, ignoring" CR));
2399+
} else {
2400+
strcpy(mqtt_topic, src);
2401+
if (mqtt_topic[strlen(mqtt_topic) - 1] != '/' && strlen(mqtt_topic) < parameters_size) {
2402+
strcat(mqtt_topic, "/");
2403+
}
23822404
}
23832405
}
23842406

@@ -3474,20 +3496,45 @@ void XtoSYS(const char* topicOri, JsonObject& SYSdata) { // json object decoding
34743496
#endif
34753497
(SYSdata.containsKey("gateway_name") && SYSdata["gateway_name"].is<const char*>()) ||
34763498
(SYSdata.containsKey("gw_pass") && SYSdata["gw_pass"].is<const char*>())) {
3499+
// Each of the destination buffers is sized at compile time; reject
3500+
// over-length MQTT-supplied values up front so a crafted publish cannot
3501+
// leave the destination non-NUL-terminated (which would corrupt the
3502+
// SPIFFS config on the next saveConfig()).
34773503
if (SYSdata.containsKey("mqtt_topic")) {
3478-
strncpy(mqtt_topic, SYSdata["mqtt_topic"], parameters_size);
3504+
const char* src = SYSdata["mqtt_topic"];
3505+
if (strlen(src) >= sizeof(mqtt_topic)) {
3506+
THEENGS_LOG_WARNING(F("mqtt_topic too long, ignoring" CR));
3507+
} else {
3508+
strcpy(mqtt_topic, src);
3509+
}
34793510
}
34803511
#ifdef ZmqttDiscovery
34813512
if (SYSdata.containsKey("discovery_prefix")) {
3482-
strncpy(discovery_prefix, SYSdata["discovery_prefix"], parameters_size);
3513+
const char* src = SYSdata["discovery_prefix"];
3514+
// discovery_prefix is defined in mqttDiscovery.cpp as char[parameters_size + 1].
3515+
if (strlen(src) > parameters_size) {
3516+
THEENGS_LOG_WARNING(F("discovery_prefix too long, ignoring" CR));
3517+
} else {
3518+
strcpy(discovery_prefix, src);
3519+
}
34833520
}
34843521
#endif
34853522
if (SYSdata.containsKey("gateway_name")) {
3486-
strncpy(gateway_name, SYSdata["gateway_name"], parameters_size);
3523+
const char* src = SYSdata["gateway_name"];
3524+
if (strlen(src) >= sizeof(gateway_name)) {
3525+
THEENGS_LOG_WARNING(F("gateway_name too long, ignoring" CR));
3526+
} else {
3527+
strcpy(gateway_name, src);
3528+
}
34873529
}
34883530
if (SYSdata.containsKey("gw_pass")) {
3489-
strncpy(ota_pass, SYSdata["gw_pass"], parameters_size);
3490-
restartESP = true;
3531+
const char* src = SYSdata["gw_pass"];
3532+
if (strlen(src) >= sizeof(ota_pass)) {
3533+
THEENGS_LOG_WARNING(F("gw_pass too long, ignoring" CR));
3534+
} else {
3535+
strcpy(ota_pass, src);
3536+
restartESP = true;
3537+
}
34913538
}
34923539
#ifndef ESPWifiManualSetup
34933540
saveConfig();
@@ -3498,7 +3545,12 @@ void XtoSYS(const char* topicOri, JsonObject& SYSdata) { // json object decoding
34983545
#if BLEDecryptor
34993546
if (SYSdata.containsKey("ble_aes") || SYSdata.containsKey("ble_aes_keys")) {
35003547
if (SYSdata.containsKey("ble_aes")) {
3501-
strncpy(ble_aes, SYSdata["ble_aes"], parameters_size);
3548+
const char* src = SYSdata["ble_aes"];
3549+
if (strlen(src) >= sizeof(ble_aes)) {
3550+
THEENGS_LOG_WARNING(F("ble_aes too long, ignoring" CR));
3551+
} else {
3552+
strcpy(ble_aes, src);
3553+
}
35023554
}
35033555
if (SYSdata.containsKey("ble_aes_keys")) {
35043556
THEENGS_LOG_WARNING(F("Contains ble_aes_keys" CR));

main/webUI.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,17 @@ void handleCG() {
880880
}
881881
if (server.hasArg("save")) {
882882
if (server.hasArg("gp") && server.arg("gp").length() > 0 && strcmp(ota_pass, server.arg("gp").c_str())) {
883-
strncpy(ota_pass, server.arg("gp").c_str(), parameters_size);
884-
pwUpdate = true;
883+
// HTML maxlength is client-side only — a direct POST can deliver any
884+
// length. Reject anything that would not fit (and leave dst
885+
// NUL-terminated) before copying into the fixed-size buffer.
886+
const char* gp = server.arg("gp").c_str();
887+
// ota_pass is extern char[] in this TU; size is parameters_size.
888+
if (strlen(gp) >= parameters_size) {
889+
THEENGS_LOG_WARNING(F("[WebUI] gateway password too long, ignoring" CR));
890+
} else {
891+
strcpy(ota_pass, gp);
892+
pwUpdate = true;
893+
}
885894
}
886895
# ifdef ZmqttDiscovery
887896
if (SYSConfig.discovery != server.hasArg("dc")) {

0 commit comments

Comments
 (0)