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:
0
tools/__init__.py
Normal file
0
tools/__init__.py
Normal 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")
|
||||||
|
|||||||
17
tools/test_flash_device.py
Normal file
17
tools/test_flash_device.py
Normal 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)
|
||||||
Reference in New Issue
Block a user