From 3e88e64a6afcfebf56d0911b7607206395fb8eca Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 20:53:53 -0400 Subject: [PATCH] =?UTF-8?q?contiki-ng+tryton:=205-MOAD=20scan=20=E2=80=94?= =?UTF-8?q?=20contiki-0001=20CWE-312=20LwM2M=20PSK=20logged;=20tryton=20MO?= =?UTF-8?q?ADs=200002-0005=20CLEAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SCAN-TODO.md | 4 +- defects/contiki-0001/patch/contiki-0001.patch | 26 ++++ .../test/ContikiLwm2mSecretKeyLogTest.class | Bin 0 -> 4468 bytes .../test/ContikiLwm2mSecretKeyLogTest.java | 138 ++++++++++++++++++ defects/contiki-ng/SCAN.md | 51 +++++++ defects/trytond/SCAN.md | 50 +++++++ 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 defects/contiki-0001/patch/contiki-0001.patch create mode 100644 defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.class create mode 100644 defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.java create mode 100644 defects/contiki-ng/SCAN.md create mode 100644 defects/trytond/SCAN.md diff --git a/SCAN-TODO.md b/SCAN-TODO.md index 663a611a1..d29613df3 100644 --- a/SCAN-TODO.md +++ b/SCAN-TODO.md @@ -15,8 +15,8 @@ Rule: clone, scan, delete clone after. Keep disk under 90%. ## Priority 2 — ERP/Business not yet scanned -- [ ] Dolibarr (PHP, ERP) -- [ ] Tryton (Python, ERP) +- [x] Dolibarr (PHP, ERP) — scanned (see prior wave) +- [x] Tryton (Python, ERP) — tryton-0001 (_save_values previous list O(T*P)); MOADs 0002/0003/0004/0005 CLEAN - [ ] xTuple PostBooks (C++/JS, ERP) - [ ] SuiteCRM (PHP, CRM) - [ ] InvoiceNinja (PHP, invoicing) diff --git a/defects/contiki-0001/patch/contiki-0001.patch b/defects/contiki-0001/patch/contiki-0001.patch new file mode 100644 index 000000000..24a644e72 --- /dev/null +++ b/defects/contiki-0001/patch/contiki-0001.patch @@ -0,0 +1,26 @@ +--- a/os/services/lwm2m/lwm2m-security.c ++++ b/os/services/lwm2m/lwm2m-security.c +@@ -204,10 +204,8 @@ write_security_object(lwm2m_object_instance_t *object, + lwm2m_object_read_string(ctx, ctx->inbuf->buffer, ctx->inbuf->size, security->public_key, LWM2M_SECURITY_KEY_SIZE); + security->public_key_len = ctx->last_value_len; + +- LOG_DBG("Writing client PKI: len: %"PRIu16" '", ctx->last_value_len); +- LOG_DBG_COAP_STRING((const char *)security->public_key, +- ctx->last_value_len); +- LOG_DBG_("'\n"); ++ /* CWE-312: do not log PKI credential material, even at DBG level */ ++ LOG_DBG("Writing client PKI: len: %"PRIu16"\n", ctx->last_value_len); + break; + case LWM2M_SECURITY_KEY_ID: + lwm2m_object_read_string(ctx, ctx->inbuf->buffer, ctx->inbuf->size, security->secret_key, LWM2M_SECURITY_KEY_SIZE); +@@ -215,10 +213,8 @@ write_security_object(lwm2m_object_instance_t *object, + +- LOG_DBG("Writing secret key: len: %"PRIu16" '", ctx->last_value_len); +- LOG_DBG_COAP_STRING((const char *)security->secret_key, +- ctx->last_value_len); +- LOG_DBG_("'\n"); ++ /* CWE-312: do not log PSK secret key material, even at DBG level */ ++ LOG_DBG("Writing secret key: len: %"PRIu16"\n", ctx->last_value_len); + + break; + } diff --git a/defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.class b/defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.class new file mode 100644 index 0000000000000000000000000000000000000000..abc249e1003a0185bbfe30fc705823385699a7c0 GIT binary patch literal 4468 zcmbtYTX0*&8UEIieNGf5Q4A(HCS(bi*ohUB3xUYF;ENnY>^N8=juMJFmX76vB^_~e zWLqWNp@Ei`Lg^*+%B7^VDW$Y^($Hq6kL9g1ec_>to1J()Xu3dYiz_lHVS$mp^Di;<<|`ntT@=Cwv@3huD$v+*tgrKM2p#Cuuqlkq=vv^YxK_Tq*Kv$0t<*b&C< z*tu|*=Mc$i*)G63RDZ-p=LBtTbFXG_rqupl^Q&F%X{JVeA22?;6iN zuDnY?E2*3FMFsC7Z|Uhgtm7_$-FrU6pN>8{bDGAfB(QeDs{K^VB1M$Il ze~MvgOqZ24q*YXgh{^V{lpW?=i3*w7m~@?r+1XwUA%*)i91dd)M+8=1j@UlirVyGz zn=$egb7WHBx{jmuiu)7{-58FBaSZn}`aSEBcK1?R)+t^~L<0drzNJ;o7;`%b0;-E*{<^xUiV#=6}le6CAghLzE>3Bk5Ptu%WfV(WI!|{Q{ zeZ%svPrR~_XzQ_I2;1d~W2(T;niCcEzMPJy1n!hFlsC)Wd2510%?RBk$5uTUcq5@1QjolcQ$Ao-r4S)_DI&@1gNz zYBa&Zr{b)p*|@53O1n~JB5!5XSEjA6&!v)498_Eo=hdB^3M1`i1qxHFusm&urQf(79%OhX5=uZ zeml!_wQ9&Jng=U|3DZd_=ml0JT_ZC+Y?S=(!2(&-(`Vb;z+~y*-af| z1q~(bijy%18J7Y#`DR_R5^hmdgj<;%Y9+N`H zkC;psh%BkpKd;hdS;H>`+84cPDOEN6O5ny%Cnf&1ci|=$)&};)U=zz8 z7EK(#$#3g83$?rH48+g5AXXgJG0+BGwMtwo(&H>4LC&%Fp{37Y*>G2LXddCVc|@A8 z;bJVXcU{}-XztpyZu`sgXc>DZAWmKUM_2Rp>ac4bYu_bzgD3w+w)1S#DPQ7xmWB1W znGH2NW3;o$y%i^U8b-qftks~2k8lks*%bJitmmvAqqUvGjertcul8mVvlH`)Pw+8~ zq}aFly!JX81E=4|hV$C^YVk1vt!A-5fc|=d5F?ia0#fO2@ z6j9TC1H}VdIc~#h>>x_FqYFDZ-oeifw#P9~|CI>p7ZB30LEQWiRak~s@$H&Yfdf=( zHMPF*9=0~$F^^qp-p4t(ySaBB{li^d=g(sQ!D!c64DSr8(|d!zMMFB!5h_N+9eLn zzs2%6ny0Bj<>okNMMP%GC;?d7eZ+9IqZKipW;f4P#J4z`3Z z-LU*5^p>!%SlPQKqW?xu2O0l(5N;fv7(oK=(R9cHXmP+fvR65cWxu2sJc`g-O z(aMj4E4I>KjmK8CuAE1ubrpvc13-xgof{_umCQoAGRAvvsGNg{a8_VsP=tAZwV~>7^5{#Zrnwt ucJUuPu$*b>2Og5I;dRbynWTQgFnhxrf9Q>;yzy-)#6vuL55J_#xBd@D#Hn)t literal 0 HcmV?d00001 diff --git a/defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.java b/defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.java new file mode 100644 index 000000000..33abd79b7 --- /dev/null +++ b/defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.java @@ -0,0 +1,138 @@ +import java.util.*; +import java.util.regex.*; + +/** + * Unit test for Contiki-NG CWE-312 defect: lwm2m-security.c logs LwM2M + * PSK secret key and PKI public key verbatim at LOG_DBG level. + * + * CWE-312: Cleartext Storage of Sensitive Information + * MOAD-0004: The Logged Secret + * + * Defect location: + * os/services/lwm2m/lwm2m-security.c write_security_object() + * LWM2M_SECURITY_CLIENT_PKI_ID case: logs public_key bytes via LOG_DBG_COAP_STRING + * LWM2M_SECURITY_KEY_ID case: logs secret_key bytes via LOG_DBG_COAP_STRING + * + * Risk: LOG_CONF_LEVEL_LWM2M defaults to LOG_LEVEL_NONE in production builds + * but is runtime-configurable and example project-conf.h files set it to + * LOG_LEVEL_DBG. When debug logging is enabled, the raw PSK secret key and + * TLS client PKI material are written to serial/UART output in cleartext. + * On constrained IoT devices these logs are often captured by gateway nodes, + * stored in cloud backends, and may appear in support tickets or monitoring + * dashboards, exposing the device's network authentication credentials. + * + * Fix: remove LOG_DBG_COAP_STRING calls for credential fields; keep only + * the length metadata which is safe to log. + */ +public class ContikiLwm2mSecretKeyLogTest { + + /** Simulate the DEFECTIVE log output: includes raw key bytes */ + static String defectiveLogOutput(byte[] secretKey, int keyLen) { + StringBuilder sb = new StringBuilder(); + sb.append("Writing secret key: len: ").append(keyLen).append(" '"); + // LOG_DBG_COAP_STRING dumps raw bytes + for (int i = 0; i < keyLen; i++) { + sb.append((char) secretKey[i]); + } + sb.append("'\n"); + return sb.toString(); + } + + /** Simulate the FIXED log output: only length, no key bytes */ + static String fixedLogOutput(byte[] secretKey, int keyLen) { + return "Writing secret key: len: " + keyLen + "\n"; + } + + /** Check whether log output contains credential material */ + static boolean containsCredentialMaterial(String logLine, byte[] key) { + // Check if any 4+ byte subsequence of the key appears in the log + if (key.length < 4) return false; + String keyStr = new String(key); + // Look for 4-char windows of the key in the log + for (int i = 0; i <= key.length - 4; i++) { + String window = new String(Arrays.copyOfRange(key, i, i + 4)); + if (logLine.contains(window)) { + return true; + } + } + return false; + } + + public static void main(String[] args) { + System.out.println("Contiki-NG CWE-312: lwm2m-security.c PSK/PKI key logged at LOG_DBG"); + System.out.println("============================================================"); + + boolean allPass = true; + + // Test 1: defective output contains secret key bytes + byte[] psk = "my-iot-preshared-key-1234".getBytes(); + String defLog = defectiveLogOutput(psk, psk.length); + boolean defectLeaks = containsCredentialMaterial(defLog, psk); + System.out.printf("DEFECT leaks PSK in log: %s (expected: true)%n", defectLeaks); + if (!defectLeaks) { + System.out.println("FAIL: defective log should contain credential material"); + allPass = false; + } + + // Test 2: fixed output does NOT contain secret key bytes + String fixLog = fixedLogOutput(psk, psk.length); + boolean fixLeaks = containsCredentialMaterial(fixLog, psk); + System.out.printf("FIXED leaks PSK in log: %s (expected: false)%n", fixLeaks); + if (fixLeaks) { + System.out.println("FAIL: fixed log must not contain credential material"); + allPass = false; + } + + // Test 3: fixed output still contains diagnostic length info + boolean fixHasLen = fixLog.contains("len: " + psk.length); + System.out.printf("FIXED retains length info: %s (expected: true)%n", fixHasLen); + if (!fixHasLen) { + System.out.println("FAIL: fixed log should retain length for diagnostics"); + allPass = false; + } + + // Test 4: PKI public_key scenario (same pattern, same fix) + // Use a key value that does not overlap with the fixed log message text + byte[] pki = "XZ99-pki-auth-secret-0xDEAD".getBytes(); + String defPkiLog = "Writing client PKI: len: " + pki.length + " '" + new String(pki) + "'\n"; + String fixPkiLog = "Writing client PKI: len: " + pki.length + "\n"; + boolean defPkiLeaks = containsCredentialMaterial(defPkiLog, pki); + boolean fixPkiLeaks = containsCredentialMaterial(fixPkiLog, pki); + System.out.printf("DEFECT PKI leaks: %s FIXED PKI leaks: %s (expected: true, false)%n", + defPkiLeaks, fixPkiLeaks); + if (!defPkiLeaks || fixPkiLeaks) { + System.out.println("FAIL: PKI credential logging assertions failed"); + allPass = false; + } + + // Test 5: binary key material (PSK is often binary, not just ASCII) + byte[] binPsk = {0x4b, 0x3a, (byte)0xff, 0x12, 0x7e, 0x01, 0x5c, (byte)0x88}; + String defBinLog = defectiveLogOutput(binPsk, binPsk.length); + String fixBinLog = fixedLogOutput(binPsk, binPsk.length); + // For binary keys, check the log length difference + boolean defBinLonger = defBinLog.length() > fixBinLog.length(); + System.out.printf("DEFECT binary PSK log longer than fixed: %s (expected: true)%n", defBinLonger); + if (!defBinLonger) { + System.out.println("FAIL: defective binary-PSK log should be longer"); + allPass = false; + } + + System.out.println("------------------------------------------------------------"); + System.out.println("Severity: MEDIUM — CWE-312 credential exposure in debug log"); + System.out.println(" Trigger: LOG_CONF_LEVEL_LWM2M >= LOG_LEVEL_DBG (set in"); + System.out.println(" examples/libs/logging/project-conf.h and common debugging"); + System.out.println(" workflows; runtime-configurable via Contiki log module)"); + System.out.println(" Impact: PSK secret and PKI material written to UART/serial"); + System.out.println(" in cleartext; gateway capture -> cloud storage -> exposure"); + System.out.println("Fix: Remove LOG_DBG_COAP_STRING for secret_key and public_key;"); + System.out.println(" retain length-only log for diagnostic value without exposure."); + System.out.println("------------------------------------------------------------"); + + if (allPass) { + System.out.println("ALL PASS"); + } else { + System.out.println("SOME TESTS FAILED"); + System.exit(1); + } + } +} diff --git a/defects/contiki-ng/SCAN.md b/defects/contiki-ng/SCAN.md new file mode 100644 index 000000000..63ff4d3ff --- /dev/null +++ b/defects/contiki-ng/SCAN.md @@ -0,0 +1,51 @@ +## Contiki-NG 5-MOAD Scan — 2026-03-31 + +Target: https://github.com/contiki-ng/contiki-ng (depth=1) +Focus: os/net/, os/lib/, os/sys/, os/services/lwm2m/ + +### MOAD-0001 (CWE-407) — CLEAN + +Neighbor table lookup (`index_from_lladdr` in `os/net/nbr-table.c`) is O(N) per +per-packet call, where N = `NBR_TABLE_CONF_MAX_NEIGHBORS` (default 16). This is +O(N) per event, not O(N^2). N is bounded by a compile-time constant, not user input. + +Route lookup (`uip_ds6_route_lookup` in `os/net/ipv6/uip-ds6-route.c`) is similarly +O(R) per packet, R bounded by `NETSTACK_MAX_ROUTE_ENTRIES` (default 16). + +TSCH link lookup (`tsch_schedule_get_link_by_handle`) is O(S*L) for S slotframes +and L links. Both are bounded small in practice for IoT deployments. + +No unbounded O(N^2) hot-path defect found. The linked-list structures are +appropriate for constrained IoT with small, fixed-size neighbor tables. + +### MOAD-0002 (Intertangle) — CLEAN + +Contiki-NG uses a protothread/event-driven cooperative scheduler. Each subsystem +(TSCH, RPL, CoAP, LwM2M, 6LoWPAN) registers independently with the netstack. +No shared mutable god object coupling independent subsystems found. + +### MOAD-0003 (Leaked Context) — CLEAN + +Contiki-NG is an embedded RTOS using cooperative protothreads, not OS threads. +No ThreadLocal, pthreads, or task-local storage patterns found. MOAD-0003 does +not apply to this architecture. + +### MOAD-0004 (CWE-312) — 1 DEFECT FOUND + +See contiki-0001. + +`os/services/lwm2m/lwm2m-security.c` `write_security_object()` logs the raw LwM2M +PSK secret key and PKI public key bytes via `LOG_DBG_COAP_STRING` at `LOG_DBG` +level. `LOG_CONF_LEVEL_LWM2M` defaults to `LOG_LEVEL_NONE` in production builds +but is runtime-configurable and is set to `LOG_LEVEL_DBG` in example projects +(`examples/libs/logging/project-conf.h`). When debug logging is active the +device's network authentication credentials are written to UART/serial in +cleartext, where they can be captured by gateway nodes and stored in cloud +infrastructure. + +### MOAD-0005 (Thundering Herd) — CLEAN + +Contiki-NG uses cooperative scheduling — only one protothread executes at a time. +There is no concurrent cache stampede possible within a single node. The SPI bus +uses `spi_arch_lock_and_open`/`spi_arch_close_and_unlock` for hardware-level +exclusive access. No concurrent get+null+compute+put pattern found. diff --git a/defects/trytond/SCAN.md b/defects/trytond/SCAN.md new file mode 100644 index 000000000..5fa6374aa --- /dev/null +++ b/defects/trytond/SCAN.md @@ -0,0 +1,50 @@ +## Tryton (trytond) 5-MOAD Scan — 2026-03-31 + +Target: https://github.com/tryton/trytond (depth=1) +Focus: trytond/model/, trytond/ir/, trytond/res/, trytond/protocols/ + +### MOAD-0001 (CWE-407) — 1 PRE-EXISTING DEFECT (tryton-0001) + +Tryton uses sets and dicts throughout hot paths. Specific checks: + +- `ir/model.py` `fill_models()`: uses a list for visited-set dedup but the list is + bounded by the number of models in a single access check (typically 1-10) and is + only called at access-check time, not in a per-record inner loop. Low severity. +- `model/tree.py` `check_recursion()`: `visited = set()` throughout. O(1) lookup. +- `model/modelsql.py`: field membership checks use dicts/sets. O(1) throughout. +- `ir/translation.py`: `trans_reports` and `strings` are dicts. O(1) lookups. +- `ir/rule.py`: domain building uses `defaultdict(list)`, no membership scans. +- `model/modelstorage.py` `_save_values()`: pre-existing defect — `previous` list + O(T*P) in one2many/many2many save. Fixed in tryton-0001. + +### MOAD-0002 (Intertangle) — CLEAN + +`Pool` is our class registry (model, wizard, report types per database). It is +intentionally a registry/locator pattern, not a god object coupling subsystems. +Modules load independently via `load_modules`. Transaction, Pool, Cache and Model +layers are cleanly separated. No shared mutable state coupling unrelated subsystems. + +### MOAD-0003 (Leaked Context) — CLEAN + +`Transaction._local = threading.local()` is an intentional per-thread transaction +stack for the WSGI worker thread model. The `_request` context key is explicitly +stripped from cache keys (`cache.py` `_key()` strips `_request`). The transaction +carries user ID, database name, and context, which are properly scoped to each +thread's request lifetime and cleared on `stop()`. This is a standard WSGI +thread-per-request pattern, not a leaked identity defect. + +### MOAD-0004 (CWE-312) — CLEAN + +`protocols/dispatcher.py` routes `common.db.login` to its own `login()` function +that does NOT call `_safe_repr` or any logger with the `parameters` dict containing +the password. `security.login()` logs only username and remote address on success +or failure, never the password or session token. No SMTP or LDAP password logged +in any logger call found in trytond core. + +### MOAD-0005 (Thundering Herd) — CLEAN + +`MemoryCache` uses a per-transaction `_database_cache` (defaultdict of LRUDict). +The transaction isolation model means each transaction sees a consistent snapshot. +Cache invalidation uses PostgreSQL `NOTIFY` channels or a polling timeout, not +concurrent get+compute+set without synchronization. No unsynchronized +cache stampede pattern found.