contiki-ng+tryton: 5-MOAD scan — contiki-0001 CWE-312 LwM2M PSK logged; tryton MOADs 0002-0005 CLEAN

This commit is contained in:
russell@unturf.com 2026-03-31 20:53:53 -04:00
parent 0f13a71cb4
commit 3e88e64a6a
6 changed files with 267 additions and 2 deletions

View file

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

View file

@ -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;
}

View file

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

View file

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

50
defects/trytond/SCAN.md Normal file
View file

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