fix: validate settings.json on load to prevent OOM and type confusion (#850)
What changed, and why it matters
This update fixes a security hole in how Krux reads its settings file from an SD card or flash storage. Before, an attacker with physical access could put an abnormally large or oddly shaped settings.json file on the card and crash or confuse the device. Now the device refuses to load any settings.json larger than 8 KB and rejects files whose contents are not a JSON object, falling back to safe default settings instead.
Treat this as a security fix and include it in the next release. Users should upgrade firmware and avoid using untrusted SD cards. Consider also logging or warning when settings.json is rejected, since silent failure could mask tampering attempts.
Security signals we found
Denial-of-service / OOM guard via file-size cap
Type-confusion guard via top-level JSON object check
Physical-access attack vector (malicious SD card)
Settings loader silently ignores malformed/invalid input
Changelog explicitly categorizes the change under Security Fixes
Evidence from the diff
The patch hardens Store._load_settings() in src/krux/settings.py. It now reads at most MAX_SETTINGS_FILE_SIZE+1 bytes (8193), discards the file if it exceeds 8192 bytes, and verifies that json.loads() returns a dict before assigning it to self.settings. Any oversized, non-object, or malformed payload is silently ignored, leaving settings as the default empty dict. Unit tests cover oversized, non-dict, and malformed JSON cases.
Changed components
src/krux/settings.py: Store._load_settings()settings.json loader on SD/flashdevice settings initializationInspect captured patch +62 / −3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4099ea..ceb769a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-# Changelog 26.03.1 - March 2025
+# Changelog 26.04.0 - April 2025
### Security Fixes
- Reject PSBT inputs with non-standard sighash types before signing
@@ -7,6 +7,7 @@
- Validate multisig quorum: reject m=0 and m>n in key-value wallet files
- DeflateIO enforces 100KB max decompressed size, preventing zip bomb OOM via BBQR encoding "Z" or KEF decryption
- Enforce part_total limits in pMofN (1–99) and BBQR (≥1) QR parsers, preventing OOM via unbounded part accumulation
+- Validate settings.json on load: enforce max file size and reject non-object payloads, preventing OOM and type-confusion via a malicious SD card
# Changelog 26.03.0 - March 2025
diff --git a/src/krux/settings.py b/src/krux/settings.py
index f19eb60..51ff5dd 100644
--- a/src/krux/settings.py
+++ b/src/krux/settings.py
@@ -35,6 +35,10 @@ FLASH_PATH = "flash"
SETTINGS_FILENAME = "settings.json"
MNEMONICS_FILE = "seeds.json"
+# Maximum accepted size of settings.json (bytes). Guards against malicious
+# oversized files on SD that could exhaust RAM during load.
+MAX_SETTINGS_FILE_SIZE = 8192
+
# Network settings
MAIN_TXT = "main"
TEST_TXT = "test"
@@ -191,10 +195,22 @@ class Store:
return "/" + location + "/"
def _load_settings(self):
- """Loads settings based on the current file_location (SD/flash)"""
+ """Loads settings based on the current file_location (SD/flash).
+
+ Rejects files larger than MAX_SETTINGS_FILE_SIZE and any payload whose
+ top-level value is not a JSON object. Per-setting type/range/category
+ validation is enforced lazily by the Setting descriptors on read.
+ """
try:
with open(self.file_location + SETTINGS_FILENAME, "r") as f:
- self.settings = json.loads(f.read())
+ # Read one extra byte to detect oversized files
+ contents = f.read(MAX_SETTINGS_FILE_SIZE + 1)
+ if len(contents) > MAX_SETTINGS_FILE_SIZE:
+ return
+ loaded = json.loads(contents)
+ if not isinstance(loaded, dict):
+ return
+ self.settings = loaded
except:
pass
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 51ce501..857962c 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -117,6 +117,48 @@ def test_string_stored_adafruit_printer_settings(mocker, m5stickv):
assert ada.line_delay == 20
+def test_oversized_settings_file_is_rejected(mocker, m5stickv):
+ """A settings.json larger than MAX_SETTINGS_FILE_SIZE must be discarded."""
+ from krux.settings import MAX_SETTINGS_FILE_SIZE
+
+ # Build a syntactically valid JSON object whose serialized form exceeds the cap
+ padding = "A" * (MAX_SETTINGS_FILE_SIZE + 100)
+ stored_settings = (
+ '{"settings": {"i18n": {"locale": "pt-BR"}}, "junk": "%s"}' % padding
+ )
+ assert len(stored_settings) > MAX_SETTINGS_FILE_SIZE
+
+ mocker.patch("builtins.open", mocker.mock_open(read_data=stored_settings))
+
+ from krux.settings import Store
+
+ store = Store()
+ # Oversized payload must be ignored: nothing loaded
+ assert store.settings == {}
+
+
+def test_non_dict_settings_file_is_rejected(mocker, m5stickv):
+ """A settings.json whose top-level JSON value isn't an object must be discarded."""
+ stored_settings = '["settings", "i18n", "pt-BR"]'
+ mocker.patch("builtins.open", mocker.mock_open(read_data=stored_settings))
+
+ from krux.settings import Store
+
+ store = Store()
+ assert store.settings == {}
+
+
+def test_malformed_settings_file_is_rejected(mocker, m5stickv):
+ """Invalid JSON must not crash the loader and must leave settings empty."""
+ stored_settings = "{not valid json"
+ mocker.patch("builtins.open", mocker.mock_open(read_data=stored_settings))
+
+ from krux.settings import Store
+
+ store = Store()
+ assert store.settings == {}
+
+
def test_stored_cnc_settings(mocker, m5stickv):
print("")
Why this scored 68/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.