mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-02 20:34:02 +00:00
# Conflicts: # MQTT_INTERNALS.md # build.sh # docs/cli_commands.md # examples/simple_repeater/MyMesh.cpp # examples/simple_repeater/MyMesh.h # examples/simple_repeater/UITask.cpp # examples/simple_room_server/MyMesh.cpp # examples/simple_room_server/MyMesh.h # examples/simple_room_server/UITask.cpp # examples/simple_sensor/SensorMesh.cpp # platformio.ini # scripts/generate_webconfig_html.py # src/MeshCore.h # src/helpers/CommonCLI.cpp # src/helpers/CommonCLI.h # src/helpers/CommonCLI_Observer.cpp # src/helpers/ESP32Board.h # src/helpers/MQTTMessageBuilder.cpp # src/helpers/MQTTPresets.h # src/helpers/NRF52Board.cpp # src/helpers/NRF52Board.h # src/helpers/bridges/MQTTBridge.cpp # src/helpers/bridges/MQTTBridge.h # src/helpers/esp32/WebConfigServer.cpp # src/helpers/esp32/WebConfigServer.h # src/helpers/radiolib/RadioLibWrappers.cpp # src/helpers/radiolib/RadioLibWrappers.h # variants/lilygo_tbeam_SX1262/platformio.ini # variants/lilygo_tbeam_SX1276/platformio.ini # variants/lilygo_tlora_v2_1/platformio.ini # webui/index.html
99 lines
4.3 KiB
C
99 lines
4.3 KiB
C
#pragma once
|
|
|
|
#include <string.h>
|
|
#include "MQTTPresets.h" // MAX_MQTT_SLOTS
|
|
|
|
// Classification of the config keys the web portal is allowed to drive through
|
|
// the CLI `set` handlers. Factored out of WebConfigServer.cpp so the allowlist
|
|
// and the (attacker-facing) key parsing can be unit-tested on the host without
|
|
// pulling in the whole ESP32 web server (see test/test_webconfig_keys).
|
|
//
|
|
// Everything here is pure string logic. The functions are `static inline` so
|
|
// each translation unit that includes this gets its own copy (there are only
|
|
// two: WebConfigServer.cpp and the test), avoiding any ODR concern.
|
|
|
|
// Keys mapping to CLI `set <key> <value>` handlers. Everything not listed here
|
|
// is rejected, so a crafted request can't reach arbitrary commands (`erase`,
|
|
// etc.) through the batch. The portal's admin-password field is classified
|
|
// separately, see wcIsAdminPasswordKey below.
|
|
static const char* const WC_ALLOWED_SET_KEYS[] = {
|
|
// NodePrefs (radio / node)
|
|
"name", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
|
|
"cad", "radio.rxgain", "radio.fem.rxgain", "repeat",
|
|
"advert.interval", "flood.advert.interval",
|
|
"flood.max", "flood.max.advert", "flood.max.unscoped", "loop.detect",
|
|
// MQTTPrefs (WiFi / MQTT / misc observer)
|
|
"wifi.ssid", "wifi.pwd", "wifi.powersave",
|
|
"mqtt.origin", "mqtt.iata", "mqtt.status", "mqtt.packets", "mqtt.raw",
|
|
"mqtt.tx", "mqtt.rx", "mqtt.interval", "mqtt.neighbors", "mqtt.neighbors.interval",
|
|
"mqtt.ntp", "mqtt.owner", "mqtt.email",
|
|
"timezone", "timezone.offset", "snmp", "snmp.community",
|
|
};
|
|
static const char* const WC_ALLOWED_SLOT_KEYS[] = {
|
|
"preset", "server", "port", "username", "password", "token", "topic", "audience",
|
|
};
|
|
|
|
// True when `key` is a well-formed per-slot key ("mqttN.<field>" with N in
|
|
// 1..MAX_MQTT_SLOTS). The shortest such key is "mqttN.x" (7 chars), and this
|
|
// probes key[4..6], so the length guard must come first -- an attacker-supplied
|
|
// "mqtt" or "m" would otherwise read past the terminator.
|
|
static inline bool wcIsSlotKeyPrefix(const char* key) {
|
|
return strlen(key) >= 7 && memcmp(key, "mqtt", 4) == 0
|
|
&& key[4] >= '1' && key[4] <= ('0' + MAX_MQTT_SLOTS) && key[5] == '.';
|
|
}
|
|
|
|
static inline bool wcIsAllowedSetKey(const char* key) {
|
|
for (size_t i = 0; i < sizeof(WC_ALLOWED_SET_KEYS) / sizeof(WC_ALLOWED_SET_KEYS[0]); i++) {
|
|
if (strcmp(key, WC_ALLOWED_SET_KEYS[i]) == 0) return true;
|
|
}
|
|
// mqtt<1-6>.<field>
|
|
if (wcIsSlotKeyPrefix(key)) {
|
|
for (size_t i = 0; i < sizeof(WC_ALLOWED_SLOT_KEYS) / sizeof(WC_ALLOWED_SLOT_KEYS[0]); i++) {
|
|
if (strcmp(&key[6], WC_ALLOWED_SLOT_KEYS[i]) == 0) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The admin password maps to the top-level `password` command, not a setter, so
|
|
// it is classified apart from the `set` allowlist. It is the only key that gets
|
|
// this treatment, which is what keeps the allowlist the sole route to `set` and
|
|
// leaves no general path from a batch to arbitrary top-level CLI commands.
|
|
static inline bool wcIsAdminPasswordKey(const char* key) {
|
|
return strcmp(key, "password") == 0;
|
|
}
|
|
|
|
static inline bool wcIsValidAdminPassword(const char* value) {
|
|
if (value == NULL) return false;
|
|
const size_t len = strlen(value);
|
|
if (len == 0 || len > 15) return false; // NodePrefs::password[16], including NUL
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (value[i] == '\r' || value[i] == '\n') return false; // reject, never silently strip
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Keys carrying a secret whose stored value is masked with the placeholder in
|
|
// the UI; a POST echoing the placeholder for one of these is dropped (unchanged).
|
|
static inline bool wcIsSecretKey(const char* key) {
|
|
if (strcmp(key, "wifi.pwd") == 0) return true;
|
|
if (wcIsSlotKeyPrefix(key)
|
|
&& (strcmp(&key[6], "password") == 0 || strcmp(&key[6], "token") == 0)) return true;
|
|
return false;
|
|
}
|
|
|
|
// Browser-generated request IDs are exactly eight random bytes encoded as
|
|
// hexadecimal. Keeping the grammar deliberately small makes the ID safe to
|
|
// echo in JSON/logs and prevents an empty or truncated ID from weakening the
|
|
// save/result correlation contract.
|
|
static inline bool wcIsValidReqId(const char* reqid) {
|
|
if (reqid == NULL || strlen(reqid) != 16) return false;
|
|
for (size_t i = 0; i < 16; i++) {
|
|
char c = reqid[i];
|
|
if (!((c >= '0' && c <= '9') ||
|
|
(c >= 'a' && c <= 'f') ||
|
|
(c >= 'A' && c <= 'F'))) return false;
|
|
}
|
|
return true;
|
|
}
|