fix(tools): reject CSV metacharacters in flash_device.py inputs

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 15:44:57 -07:00
parent 641ab29277
commit 56fc58b843
3 changed files with 43 additions and 0 deletions

0
tools/__init__.py Normal file
View File

View File

@@ -28,6 +28,25 @@ NVS_NAMESPACE = "doorcounter"
NVS_PARTITION_OFFSET = "0x9000" NVS_PARTITION_OFFSET = "0x9000"
NVS_PARTITION_SIZE = "0x5000" # matches firmware partition table (20KB) 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, def build_nvs_csv(device_id, location_id, hmac_secret,
wifi_ssid=None, wifi_pass=None, line_offset=50): 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) print("Error: --line-offset must be 0-100", file=sys.stderr)
sys.exit(1) 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: with tempfile.TemporaryDirectory() as tmp:
csv_path = os.path.join(tmp, "nvs.csv") csv_path = os.path.join(tmp, "nvs.csv")
bin_path = os.path.join(tmp, "nvs.bin") bin_path = os.path.join(tmp, "nvs.bin")

View File

@@ -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)