From 56fc58b8432374dd336bcf56f972925f4a3ee737 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Fri, 1 May 2026 15:44:57 -0700 Subject: [PATCH] fix(tools): reject CSV metacharacters in flash_device.py inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit device-id, location-id, wifi-ssid, and wifi-password were interpolated directly into the NVS partition CSV. A value containing comma, double quote, CR, or LF would split the field/row and silently provision the wrong NVS keys — easiest concrete failure: a Wi-Fi password containing a comma. Validate operator-supplied strings before generating the CSV. Add an empty tools/__init__.py so the regression tests can import the helper as 'tools.flash_device' (matches the existing 'server.*' test pattern). Found via adversarial review (run 2026-05-01-192928, gpt-5.5 reviewer). Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/__init__.py | 0 tools/flash_device.py | 26 ++++++++++++++++++++++++++ tools/test_flash_device.py | 17 +++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 tools/__init__.py create mode 100644 tools/test_flash_device.py diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/flash_device.py b/tools/flash_device.py index f6fee27..bc4724b 100755 --- a/tools/flash_device.py +++ b/tools/flash_device.py @@ -28,6 +28,25 @@ NVS_NAMESPACE = "doorcounter" NVS_PARTITION_OFFSET = "0x9000" NVS_PARTITION_SIZE = "0x5000" # matches firmware partition table (20KB) +# Characters that would change the field/row structure of the NVS-CSV format +# (key,type,encoding,value). A value containing any of these would either +# split into more fields or add rows, silently provisioning the wrong keys. +_CSV_FORBIDDEN = (",", '"', "\n", "\r") + + +def _reject_csv_metacharacters(name, value): + """Exit with an error if value contains a character that would corrupt + the NVS CSV. Used for operator-supplied strings (device id, location id, + WiFi credentials).""" + for c in _CSV_FORBIDDEN: + if c in value: + print( + f"Error: --{name} contains forbidden character {c!r}; " + f"this would corrupt the NVS partition CSV.", + file=sys.stderr, + ) + sys.exit(1) + def build_nvs_csv(device_id, location_id, hmac_secret, wifi_ssid=None, wifi_pass=None, line_offset=50): @@ -78,6 +97,13 @@ def main(): print("Error: --line-offset must be 0-100", file=sys.stderr) sys.exit(1) + _reject_csv_metacharacters("device-id", args.device_id) + _reject_csv_metacharacters("location-id", args.location_id) + if args.wifi_ssid is not None: + _reject_csv_metacharacters("wifi-ssid", args.wifi_ssid) + if args.wifi_password is not None: + _reject_csv_metacharacters("wifi-password", args.wifi_password) + with tempfile.TemporaryDirectory() as tmp: csv_path = os.path.join(tmp, "nvs.csv") bin_path = os.path.join(tmp, "nvs.bin") diff --git a/tools/test_flash_device.py b/tools/test_flash_device.py new file mode 100644 index 0000000..45e29e2 --- /dev/null +++ b/tools/test_flash_device.py @@ -0,0 +1,17 @@ +import pytest + +from tools.flash_device import _reject_csv_metacharacters + + +def test_clean_value_accepted(): + """A value with no metacharacters should pass without exiting.""" + _reject_csv_metacharacters("device-id", "dc-0042") + _reject_csv_metacharacters("location-id", "retailer-123") + _reject_csv_metacharacters("wifi-ssid", "StoreWiFi-2.4GHz") + _reject_csv_metacharacters("wifi-password", "p@ssw0rd!~#$%^&*()_+-=:;<>?/") + + +@pytest.mark.parametrize("bad", ["Home,Network", 'pa"ss', "ssid\nfoo", "name\rbar"]) +def test_metacharacter_rejected(bad): + with pytest.raises(SystemExit): + _reject_csv_metacharacters("wifi-ssid", bad)