diff --git a/defects/freertos/CLEAN.md b/defects/freertos/CLEAN.md new file mode 100644 index 000000000..c8e6cebfb --- /dev/null +++ b/defects/freertos/CLEAN.md @@ -0,0 +1,45 @@ +# FreeRTOS-Kernel — All 5 MOADs CLEAN + +**Target:** FreeRTOS-Kernel (https://github.com/FreeRTOS/FreeRTOS-Kernel) +**Scan date:** 2026-03-31 +**Commit:** depth=1 HEAD + +## MOAD-0001 (CWE-407) — CLEAN + +No O(N²) list-contains-inside-loop patterns found in hot paths. + +- `pxReadyTasksLists` uses a priority-indexed array: O(1) lookup by priority. +- `listGET_OWNER_OF_NEXT_ENTRY` is a round-robin next pointer — O(1) per tick. +- `event_groups.c` `xEventGroupSetBits` iterates waiting tasks once — O(T) not O(T²). +- `xTaskGetHandle` does O(P×N) name scan across all priority lists — called only + by name, not per-tick. No outer loop calls it again. +- Timer lists use sorted insertion by expiry; `prvInsertTimerInActiveList` is O(N) + but timers are inserted infrequently. + +## MOAD-0002 (Intertangle) — CLEAN + +Global task lists (`pxReadyTasksLists`, `xSuspendedTaskList`, etc.) are the +intentional scheduler state, not a god-object coupling independent subsystems. +Each kernel primitive (queues, semaphores, mutexes, event groups, timers) owns its +own list and communicates through the task state machine cleanly. + +## MOAD-0003 (Leaked Context) — CLEAN + +`pvThreadLocalStoragePointers` is per-task storage sized at compile time. No pattern +of request-scoped identity stored in task-local slots and then accessed by a +different request was found. The C-runtime TLS block (`xTLSBlock`) is correctly +swapped on every context switch via `configSET_TLS_BLOCK`. + +## MOAD-0004 (CWE-312) — CLEAN + +No WiFi passwords, TLS keys, or credentials in kernel source. Examples directory +contains only a trivial task-blink `cmake_example/main.c` with no credentials. +Portable BSP files reference hardware register maps (password field is a hardware +register name, not a user credential). + +## MOAD-0005 (Thundering Herd) — CLEAN + +All shared state is protected by `taskENTER_CRITICAL` / `taskEXIT_CRITICAL` or +scheduler suspension (`vTaskSuspendAll`). Queue/mutex operations are atomic. Timer +command queue uses a proper send/receive pattern — no unsynchronized +get+null+alloc+put cache pattern found. diff --git a/defects/jitsi-videobridge/MOAD-0002-CLEAN.md b/defects/jitsi-videobridge/MOAD-0002-CLEAN.md new file mode 100644 index 000000000..78f9450cf --- /dev/null +++ b/defects/jitsi-videobridge/MOAD-0002-CLEAN.md @@ -0,0 +1,7 @@ +# jitsi-videobridge MOAD-0002 (Intertangle) — CLEAN + +Scanned 2026-03-31. No shared mutable global state coupling independent subsystems found. + +- `BandwidthAllocator`, `ConferenceSpeechActivity`, `DtlsServer`, and XMPP transport are well-separated classes +- Each conference has its own `ConferenceSpeechActivity` instance (not shared globally) +- No god-object pattern coupling unrelated subsystems through shared mutable state diff --git a/defects/jitsi-videobridge/MOAD-0003-CLEAN.md b/defects/jitsi-videobridge/MOAD-0003-CLEAN.md new file mode 100644 index 000000000..865003076 --- /dev/null +++ b/defects/jitsi-videobridge/MOAD-0003-CLEAN.md @@ -0,0 +1,7 @@ +# jitsi-videobridge MOAD-0003 (Leaked Context) — CLEAN + +Scanned 2026-03-31. No ThreadLocal holding request-scoped or conference-scoped identity found. + +- Uses `synchronized` blocks and Kotlin coroutines for concurrency +- `ThreadLocalRandom` found only in `PartitionedByteBufferPool.java` and `DcSctpTransport.kt` (stateless random use — not context leakage) +- Conference context passed explicitly via constructor injection, not via ThreadLocal diff --git a/defects/jitsi-videobridge/MOAD-0004-CLEAN.md b/defects/jitsi-videobridge/MOAD-0004-CLEAN.md new file mode 100644 index 000000000..b03a5a8b5 --- /dev/null +++ b/defects/jitsi-videobridge/MOAD-0004-CLEAN.md @@ -0,0 +1,7 @@ +# jitsi-videobridge MOAD-0004 (CWE-312) — CLEAN + +Scanned 2026-03-31. No credential or secret material logged verbatim found. + +- DTLS fingerprints logged at debug level are SHA-256 hashes of public keys — not secrets +- XMPP authentication does not log passwords or tokens +- No API keys, auth tokens, or private key material found in log statements diff --git a/defects/jitsi-videobridge/MOAD-0005-CLEAN.md b/defects/jitsi-videobridge/MOAD-0005-CLEAN.md new file mode 100644 index 000000000..3a20e1bbc --- /dev/null +++ b/defects/jitsi-videobridge/MOAD-0005-CLEAN.md @@ -0,0 +1,8 @@ +# jitsi-videobridge MOAD-0005 (Thundering Herd) — CLEAN + +Scanned 2026-03-31. No unsynchronized cache get+null+compute+put patterns found. + +- `KeyframeRequester.kt` uses `ConcurrentHashMap.computeIfAbsent()` correctly for per-SSRC limiters +- `RetransmissionRequester.kt` uses `computeIfAbsent()` for stream packet requesters +- `RtcpRrGenerator.kt` uses `computeIfAbsent()` for sender info tracking +- All hot-path maps use proper atomic operations diff --git a/defects/woodpecker-0001/MOAD-0002-CLEAN.md b/defects/woodpecker-0001/MOAD-0002-CLEAN.md new file mode 100644 index 000000000..3cd114d2b --- /dev/null +++ b/defects/woodpecker-0001/MOAD-0002-CLEAN.md @@ -0,0 +1,7 @@ +# woodpecker MOAD-0002 (Intertangle) — CLEAN + +Scanned 2026-03-31. No god-object coupling independent subsystems found. + +- `server.Config` is a static config struct — read-only after startup, not a runtime god object +- Queue, pubsub, forge, and pipeline subsystems are separated via interfaces +- No shared mutable global state found coupling unrelated subsystems at runtime diff --git a/defects/woodpecker-0001/MOAD-0003-CLEAN.md b/defects/woodpecker-0001/MOAD-0003-CLEAN.md new file mode 100644 index 000000000..aaf037e69 --- /dev/null +++ b/defects/woodpecker-0001/MOAD-0003-CLEAN.md @@ -0,0 +1,7 @@ +# woodpecker MOAD-0003 (Leaked Context) — CLEAN + +Scanned 2026-03-31. No goroutine-local storage or ContextVar abuse found. + +- Uses standard `context.Context` passed explicitly through call chains +- No goroutine-local storage or request-scoped identity leakage via global state +- `context.WithValue` used only for CLI backend type at agent startup (one-time config, not request identity) diff --git a/defects/woodpecker-0001/MOAD-0005-CLEAN.md b/defects/woodpecker-0001/MOAD-0005-CLEAN.md new file mode 100644 index 000000000..55d05ba70 --- /dev/null +++ b/defects/woodpecker-0001/MOAD-0005-CLEAN.md @@ -0,0 +1,8 @@ +# woodpecker MOAD-0005 (Thundering Herd) — CLEAN + +Scanned 2026-03-31. No unsynchronized cache get+null+compute+put found. + +- Token refresh uses `singleflight.Group` correctly — concurrent refresh calls for the same user deduplicate to one request +- `sync.Map` used for step/workflow state in local and dummy backends +- Queue operations protected by mutex +- No raw map get+nil+put patterns in concurrent hot paths diff --git a/defects/woodpecker-0001/patch/0001-step-builder-filter-items-hashmap.patch b/defects/woodpecker-0001/patch/0001-step-builder-filter-items-hashmap.patch new file mode 100644 index 000000000..bb91bb909 --- /dev/null +++ b/defects/woodpecker-0001/patch/0001-step-builder-filter-items-hashmap.patch @@ -0,0 +1,54 @@ +diff --git a/server/pipeline/step_builder/step_builder.go b/server/pipeline/step_builder/step_builder.go +index abcdef0..1234567 100644 +--- a/server/pipeline/step_builder/step_builder.go ++++ b/server/pipeline/step_builder/step_builder.go +@@ -227,27 +227,27 @@ func (b *StepBuilder) environmentVariables(metadata metadata.Metadata, axis matr + // filterItemsWithMissingDependencies removes items whose depends_on names are + // not present in the item list. Recurses to handle transitive removals. +-// CWE-407: containsItemWithName is O(N) per call; called inside two nested loops +-// giving O(N * D * N) = O(N²) overall where D = deps per item. ++// Fix: build a name-set map upfront for O(1) membership tests; O(N) total. + func filterItemsWithMissingDependencies(items []*Item) []*Item { +- itemsToRemove := make([]*Item, 0) ++ // Build O(1) lookup set of all item names. ++ nameSet := make(map[string]struct{}, len(items)) ++ for _, item := range items { ++ nameSet[item.Workflow.Name] = struct{}{} ++ } + ++ toRemoveNames := make(map[string]struct{}) + for _, item := range items { + for _, dep := range item.DependsOn { +- if !containsItemWithName(dep, items) { +- itemsToRemove = append(itemsToRemove, item) ++ if _, exists := nameSet[dep]; !exists { ++ toRemoveNames[item.Workflow.Name] = struct{}{} ++ break + } + } + } + +- if len(itemsToRemove) > 0 { ++ if len(toRemoveNames) > 0 { + filtered := make([]*Item, 0, len(items)) + for _, item := range items { +- if !containsItemWithName(item.Workflow.Name, itemsToRemove) { ++ if _, remove := toRemoveNames[item.Workflow.Name]; !remove { + filtered = append(filtered, item) + } + } + // Recursive to handle transitive deps + return filterItemsWithMissingDependencies(filtered) + } + + return items + } +- +-func containsItemWithName(name string, items []*Item) bool { +- for _, item := range items { +- if name == item.Workflow.Name { +- return true +- } +- } +- return false +-} diff --git a/defects/woodpecker-0001/test/go.mod b/defects/woodpecker-0001/test/go.mod new file mode 100644 index 000000000..7dee36a6c --- /dev/null +++ b/defects/woodpecker-0001/test/go.mod @@ -0,0 +1,3 @@ +module woodpecker_test + +go 1.21 diff --git a/defects/woodpecker-0001/test/woodpecker_step_builder_test.go b/defects/woodpecker-0001/test/woodpecker_step_builder_test.go new file mode 100644 index 000000000..e7b73ceb1 --- /dev/null +++ b/defects/woodpecker-0001/test/woodpecker_step_builder_test.go @@ -0,0 +1,165 @@ +// CWE-407 benchmark: woodpecker-0001 +// filterItemsWithMissingDependencies uses containsItemWithName (O(N) linear scan) +// inside two nested loops: O(N * D * N) = O(N²) where D = average deps per item. +// Fix: pre-build a name-set map for O(1) membership tests, reducing to O(N). +// +// Run: go test -v -run TestFilterItems ./... +package woodpecker_test + +import ( + "fmt" + "testing" +) + +// Item mirrors the step_builder Item struct for benchmarking purposes. +type Item struct { + Name string + DependsOn []string +} + +// slowContainsItemWithName is the original O(N) linear scan. +func slowContainsItemWithName(name string, items []*Item) bool { + for _, item := range items { + if name == item.Name { + return true + } + } + return false +} + +// slowFilterItems is the original O(N²) implementation. +func slowFilterItems(items []*Item) ([]*Item, int64) { + var ops int64 + itemsToRemove := make([]*Item, 0) + + for _, item := range items { + for _, dep := range item.DependsOn { + ops++ // containsItemWithName call + if !slowContainsItemWithName(dep, items) { + ops += int64(len(items)) // O(N) scan + itemsToRemove = append(itemsToRemove, item) + } else { + ops += int64(len(items)) // worst-case scan + } + } + } + + if len(itemsToRemove) > 0 { + filtered := make([]*Item, 0) + for _, item := range items { + ops += int64(len(itemsToRemove)) // O(N) scan per item + if !slowContainsItemWithName(item.Name, itemsToRemove) { + filtered = append(filtered, item) + } + } + sub, subOps := slowFilterItems(filtered) + ops += subOps + return sub, ops + } + + return items, ops +} + +// fastFilterItems is the O(N) fixed implementation using a name map. +func fastFilterItems(items []*Item) ([]*Item, int64) { + var ops int64 + + // Build O(1) lookup set - O(N) once. + nameSet := make(map[string]struct{}, len(items)) + for _, item := range items { + nameSet[item.Name] = struct{}{} + ops++ + } + + toRemoveNames := make(map[string]struct{}) + for _, item := range items { + for _, dep := range item.DependsOn { + ops++ // O(1) map lookup + if _, exists := nameSet[dep]; !exists { + toRemoveNames[item.Name] = struct{}{} + break + } + } + } + + if len(toRemoveNames) > 0 { + filtered := make([]*Item, 0, len(items)) + for _, item := range items { + ops++ // O(1) map lookup + if _, remove := toRemoveNames[item.Name]; !remove { + filtered = append(filtered, item) + } + } + sub, subOps := fastFilterItems(filtered) + ops += subOps + return sub, ops + } + + return items, ops +} + +// buildItems creates N items where items[N/4..N/2] have a missing dep ("ghost") +// to trigger the filter path, and the rest have valid deps on previous items. +func buildItems(n int) []*Item { + items := make([]*Item, n) + for i := 0; i < n; i++ { + deps := []string{} + if i > 0 { + deps = append(deps, fmt.Sprintf("item-%d", i-1)) + } + items[i] = &Item{Name: fmt.Sprintf("item-%d", i), DependsOn: deps} + } + // Inject missing deps in the middle quarter to trigger filtering. + for i := n / 4; i < n/2; i++ { + items[i].DependsOn = append(items[i].DependsOn, "ghost-missing-dep") + } + return items +} + +func TestFilterItemsCorrectness(t *testing.T) { + items := buildItems(20) + slow, _ := slowFilterItems(items) + fast, _ := fastFilterItems(items) + + if len(slow) != len(fast) { + t.Fatalf("result length mismatch: slow=%d fast=%d", len(slow), len(fast)) + } + + slowNames := make(map[string]bool) + for _, it := range slow { + slowNames[it.Name] = true + } + for _, it := range fast { + if !slowNames[it.Name] { + t.Errorf("fast result contains item not in slow result: %s", it.Name) + } + } + t.Logf("correctness: slow=%d items, fast=%d items — match", len(slow), len(fast)) +} + +func TestFilterItemsOpCount(t *testing.T) { + sizes := []int{10, 30, 50, 100} + for _, n := range sizes { + items := buildItems(n) + _, slowOps := slowFilterItems(items) + _, fastOps := fastFilterItems(items) + ratio := float64(slowOps) / float64(fastOps) + t.Logf("N=%3d slow=%6d ops fast=%6d ops ratio=%.1fx", n, slowOps, fastOps, ratio) + if n >= 50 && ratio < 3.0 { + t.Errorf("N=%d: expected ratio >= 3x, got %.1fx (slow=%d, fast=%d)", n, ratio, slowOps, fastOps) + } + } +} + +func TestFilterItemsPass(t *testing.T) { + n := 100 + items := buildItems(n) + _, slowOps := slowFilterItems(items) + _, fastOps := fastFilterItems(items) + ratio := float64(slowOps) / float64(fastOps) + t.Logf("woodpecker-0001 N=%d: slow=%d ops, fast=%d ops, ratio=%.1fx", n, slowOps, fastOps, ratio) + if ratio < 5.0 { + t.Fatalf("FAIL: expected at least 5x op-count reduction, got %.1fx", ratio) + } + t.Logf("PASS: %.0fx op-count reduction at N=%d", ratio, n) +} diff --git a/defects/woodpecker-0002/patch/0001-token-parserequest-cwe312-fix.patch b/defects/woodpecker-0002/patch/0001-token-parserequest-cwe312-fix.patch new file mode 100644 index 000000000..be5f5151d --- /dev/null +++ b/defects/woodpecker-0002/patch/0001-token-parserequest-cwe312-fix.patch @@ -0,0 +1,15 @@ +diff --git a/shared/token/token.go b/shared/token/token.go +index abcdef0..1234567 100644 +--- a/shared/token/token.go ++++ b/shared/token/token.go +@@ -68,8 +68,8 @@ func ParseRequest(allowedTypes []Type, r *http.Request, fn SecretFunc) (*Token, + // first we attempt to get the token from the + // authorization header. + token := r.Header.Get("Authorization") + if len(token) != 0 { +- log.Trace().Msgf("token.ParseRequest: found token in header: %s", token) ++ // CWE-312 fix: do NOT log the Authorization header value — it contains the raw Bearer token. ++ log.Trace().Msg("token.ParseRequest: found token in Authorization header") + bearer := token + if _, err := fmt.Sscanf(token, "Bearer %s", &bearer); err != nil { + return nil, err diff --git a/defects/woodpecker-0002/test/go.mod b/defects/woodpecker-0002/test/go.mod new file mode 100644 index 000000000..c99eb3ab9 --- /dev/null +++ b/defects/woodpecker-0002/test/go.mod @@ -0,0 +1,3 @@ +module woodpecker_token_test + +go 1.21 diff --git a/defects/woodpecker-0002/test/woodpecker_token_cwe312_test.go b/defects/woodpecker-0002/test/woodpecker_token_cwe312_test.go new file mode 100644 index 000000000..77d256bee --- /dev/null +++ b/defects/woodpecker-0002/test/woodpecker_token_cwe312_test.go @@ -0,0 +1,69 @@ +// CWE-312 test: woodpecker-0002 +// shared/token/token.go ParseRequest() logs the raw Authorization header value +// at Trace level: log.Trace().Msgf("token.ParseRequest: found token in header: %s", token) +// This exposes the raw Bearer token (credential) in application logs. +// Fix: redact — log only that a header was found, not its value. +// +// Run: go test -v -run TestTokenLogging ./... +package woodpecker_token_test + +import ( + "fmt" + "strings" + "testing" +) + +// simulateVulnerableLog simulates the vulnerable logging pattern. +func simulateVulnerableLog(authHeader string) string { + // Original code: log.Trace().Msgf("token.ParseRequest: found token in header: %s", token) + return fmt.Sprintf("token.ParseRequest: found token in header: %s", authHeader) +} + +// simulateFixedLog simulates the fixed logging pattern. +func simulateFixedLog(_ string) string { + // Fixed: log.Trace().Msg("token.ParseRequest: found token in Authorization header") + return "token.ParseRequest: found token in Authorization header" +} + +func TestVulnerableLogExposesToken(t *testing.T) { + secret := "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.secret-payload.signature" + logLine := simulateVulnerableLog(secret) + + if !strings.Contains(logLine, "secret-payload") { + t.Fatalf("expected vulnerable log to contain token value, got: %s", logLine) + } + t.Logf("CONFIRMED vulnerable: log line contains raw token: %s", logLine) +} + +func TestFixedLogDoesNotExposeToken(t *testing.T) { + secret := "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.secret-payload.signature" + logLine := simulateFixedLog(secret) + + if strings.Contains(logLine, "secret-payload") { + t.Fatalf("FAIL: fixed log still contains token value: %s", logLine) + } + if !strings.Contains(logLine, "Authorization header") { + t.Fatalf("FAIL: fixed log should mention Authorization header: %s", logLine) + } + t.Logf("PASS: fixed log does not expose token: %s", logLine) +} + +func TestCWE312Pass(t *testing.T) { + // Simulate a high-entropy Bearer token (JWT). + token := "Bearer " + strings.Repeat("A", 128) + ".secret." + strings.Repeat("B", 64) + + vulnerable := simulateVulnerableLog(token) + fixed := simulateFixedLog(token) + + if !strings.Contains(vulnerable, "secret") { + t.Fatalf("vulnerable pattern must log the token value") + } + if strings.Contains(fixed, "secret") || strings.Contains(fixed, strings.Repeat("A", 10)) { + t.Fatalf("fixed pattern must NOT log any token content") + } + + t.Logf("woodpecker-0002 CWE-312:") + t.Logf(" vulnerable log line: %q", vulnerable[:80]+"...") + t.Logf(" fixed log line: %q", fixed) + t.Log(" PASS: credential redacted in fixed variant") +} diff --git a/defects/zephyr-0001/patch/zephyr-0001.patch b/defects/zephyr-0001/patch/zephyr-0001.patch new file mode 100644 index 000000000..271bc85ef --- /dev/null +++ b/defects/zephyr-0001/patch/zephyr-0001.patch @@ -0,0 +1,58 @@ +--- a/subsys/net/lib/wifi_credentials/wifi_credentials_shell.c ++++ b/subsys/net/lib/wifi_credentials/wifi_credentials_shell.c +@@ -49,17 +49,17 @@ static void print_network_info(void *cb_arg, const char *ssid, size_t ssid_len) + # Defect ID: zephyr-0001 + # MOAD: 0004 + # Severity: HIGH + # CVE class: CWE-312 Cleartext Storage/Exposure of Sensitive Information + # + # Root cause: print_network_info() in wifi_credentials_shell.c retrieves stored + # WiFi credentials and prints the plaintext PSK/password unconditionally via + # shell_fprintf when the "wifi cred list" shell command is run: + # + # line 57-59: shell_fprintf(sh, ..., ", password: \"%.*s\", password_len: %d", + # ..., creds.password, creds.password_len) + # + # For WPA2-PSK, WPA2-PSK-SHA256, SAE, and WPA-PSK security types the password + # is emitted verbatim to the shell. Additionally for EAP-TLS enterprise mode: + # + # line 65-68: shell_fprintf(sh, ..., ", key_passwd: \"%.*s\"...", + # ..., creds.header.key_passwd, ...) + # + # This exposes the private-key passphrase for enterprise TLS connections. + # + # On embedded systems with serial console or UART shell, any observer with + # console access sees live credentials. On boards with logging backends that + # write to flash or network syslog the credential is persisted in cleartext. + # + # Fix: replace the password and key_passwd fields with a redacted marker + # ("[redacted]") when printing via the shell. The password_len is still + # printed so the operator can verify a credential is set without exposing value. + ++static const char REDACTED[] = "[redacted]"; ++ + static void print_network_info(void *cb_arg, const char *ssid, size_t ssid_len) + { + int ret = 0; +@@ -53,12 +53,12 @@ static void print_network_info(void *cb_arg, const char *ssid, size_t ssid_len) + if (creds.header.type == WIFI_SECURITY_TYPE_PSK || + creds.header.type == WIFI_SECURITY_TYPE_PSK_SHA256 || + creds.header.type == WIFI_SECURITY_TYPE_SAE || + creds.header.type == WIFI_SECURITY_TYPE_WPA_PSK) { + shell_fprintf(sh, SHELL_VT100_COLOR_DEFAULT, +- ", password: \"%.*s\", password_len: %d", (int)creds.password_len, +- creds.password, creds.password_len); ++ ", password: %s, password_len: %d", ++ REDACTED, creds.password_len); + } + + #ifdef CONFIG_WIFI_NM_WPA_SUPPLICANT_CRYPTO_ENTERPRISE + if (creds.header.type == WIFI_SECURITY_TYPE_EAP_TLS) { + if (creds.header.key_passwd_length > 0) { + shell_fprintf(sh, SHELL_VT100_COLOR_DEFAULT, +- ", key_passwd: \"%.*s\", key_passwd_len: %d", +- creds.header.key_passwd_length, creds.header.key_passwd, +- creds.header.key_passwd_length); ++ ", key_passwd: %s, key_passwd_len: %d", ++ REDACTED, creds.header.key_passwd_length); + } diff --git a/defects/zephyr-0001/test/ZephyrWifiCredShellTest.class b/defects/zephyr-0001/test/ZephyrWifiCredShellTest.class new file mode 100644 index 000000000..f89738006 Binary files /dev/null and b/defects/zephyr-0001/test/ZephyrWifiCredShellTest.class differ diff --git a/defects/zephyr-0001/test/ZephyrWifiCredShellTest.java b/defects/zephyr-0001/test/ZephyrWifiCredShellTest.java new file mode 100644 index 000000000..2bd811c61 --- /dev/null +++ b/defects/zephyr-0001/test/ZephyrWifiCredShellTest.java @@ -0,0 +1,187 @@ +import java.util.*; + +/** + * Unit test for zephyr-0001: wifi_credentials_shell.c print_network_info() + * prints WiFi PSK and EAP-TLS key_passwd verbatim to the shell (CWE-312). + * + * Models the C pattern in print_network_info(): + * + * shell_fprintf(sh, ..., ", password: \"%.*s\", password_len: %d", + * (int)creds.password_len, creds.password, creds.password_len); + * + * When a user runs "wifi cred list" on a Zephyr shell, all stored network + * credentials including WPA2-PSK passwords and EAP-TLS private-key passphrases + * are printed in cleartext. On serial/UART consoles this is visible to anyone + * with physical access; logging backends may persist the secret. + * + * Fix: replace the credential value with "[redacted]", keep the length. + */ +public class ZephyrWifiCredShellTest { + + static final String REDACTED = "[redacted]"; + + // Security type constants (mirroring Zephyr wifi_security_type) + static final int WIFI_SECURITY_TYPE_NONE = 0; + static final int WIFI_SECURITY_TYPE_PSK = 1; + static final int WIFI_SECURITY_TYPE_PSK_SHA256 = 2; + static final int WIFI_SECURITY_TYPE_SAE = 3; + static final int WIFI_SECURITY_TYPE_WPA_PSK = 9; + static final int WIFI_SECURITY_TYPE_EAP_TLS = 7; + + // --- Defective version: prints password verbatim --- + static String printNetworkInfoDefective(String ssid, int secType, + String password, String keyPasswd) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format(" network ssid: \"%s\", ssid_len: %d, type: %d", + ssid, ssid.length(), secType)); + + if (secType == WIFI_SECURITY_TYPE_PSK || + secType == WIFI_SECURITY_TYPE_PSK_SHA256 || + secType == WIFI_SECURITY_TYPE_SAE || + secType == WIFI_SECURITY_TYPE_WPA_PSK) { + // DEFECT: password value logged verbatim + sb.append(String.format(", password: \"%s\", password_len: %d", + password, password.length())); + } + + if (secType == WIFI_SECURITY_TYPE_EAP_TLS && keyPasswd != null && !keyPasswd.isEmpty()) { + // DEFECT: key_passwd value logged verbatim + sb.append(String.format(", key_passwd: \"%s\", key_passwd_len: %d", + keyPasswd, keyPasswd.length())); + } + + return sb.toString(); + } + + // --- Fixed version: redacts password and key_passwd --- + static String printNetworkInfoFixed(String ssid, int secType, + String password, String keyPasswd) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format(" network ssid: \"%s\", ssid_len: %d, type: %d", + ssid, ssid.length(), secType)); + + if (secType == WIFI_SECURITY_TYPE_PSK || + secType == WIFI_SECURITY_TYPE_PSK_SHA256 || + secType == WIFI_SECURITY_TYPE_SAE || + secType == WIFI_SECURITY_TYPE_WPA_PSK) { + // FIX: redact password value, keep length + sb.append(String.format(", password: %s, password_len: %d", + REDACTED, password.length())); + } + + if (secType == WIFI_SECURITY_TYPE_EAP_TLS && keyPasswd != null && !keyPasswd.isEmpty()) { + // FIX: redact key_passwd value, keep length + sb.append(String.format(", key_passwd: %s, key_passwd_len: %d", + REDACTED, keyPasswd.length())); + } + + return sb.toString(); + } + + public static void main(String[] args) { + System.out.println("=== zephyr-0001: wifi_credentials_shell PSK/key_passwd CWE-312 ===\n"); + + int pass = 0, total = 0; + + // Test 1: WPA2-PSK defective path leaks password + { + total++; + String psk = "SuperSecretWifi123!"; + String out = printNetworkInfoDefective("HomeNetwork", WIFI_SECURITY_TYPE_PSK, psk, null); + boolean leaks = out.contains(psk); + assert leaks : "FAIL T1: expected defective path to contain PSK"; + System.out.println("T1 CONFIRMED defective: PSK in output = " + leaks + " out=" + out); + pass++; + } + + // Test 2: WPA2-PSK fixed path redacts password + { + total++; + String psk = "SuperSecretWifi123!"; + String out = printNetworkInfoFixed("HomeNetwork", WIFI_SECURITY_TYPE_PSK, psk, null); + boolean hidden = !out.contains(psk); + boolean hasRedacted = out.contains(REDACTED); + boolean hasLen = out.contains(String.valueOf(psk.length())); + assert hidden : "FAIL T2: fixed path should not contain PSK"; + assert hasRedacted : "FAIL T2: fixed path should contain [redacted]"; + assert hasLen : "FAIL T2: fixed path should still show password_len"; + System.out.printf("T2 PASS fixed: hidden=%b redacted=%b len_present=%b%n", + hidden, hasRedacted, hasLen); + pass++; + } + + // Test 3: SAE (WPA3) password redacted + { + total++; + String saePass = "WPA3SensitivePass"; + String out = printNetworkInfoFixed("Office5G", WIFI_SECURITY_TYPE_SAE, saePass, null); + assert !out.contains(saePass) : "FAIL T3: SAE password should be redacted"; + assert out.contains(REDACTED) : "FAIL T3: [redacted] marker expected"; + System.out.println("T3 PASS: SAE password redacted correctly"); + pass++; + } + + // Test 4: EAP-TLS key_passwd defective path leaks passphrase + { + total++; + String keyPass = "PrivKeyPassphrase42"; + String out = printNetworkInfoDefective("EnterpriseNet", WIFI_SECURITY_TYPE_EAP_TLS, "", keyPass); + boolean leaks = out.contains(keyPass); + assert leaks : "FAIL T4: expected defective EAP-TLS path to contain key_passwd"; + System.out.println("T4 CONFIRMED defective: EAP key_passwd in output = " + leaks); + pass++; + } + + // Test 5: EAP-TLS key_passwd fixed path redacts passphrase + { + total++; + String keyPass = "PrivKeyPassphrase42"; + String out = printNetworkInfoFixed("EnterpriseNet", WIFI_SECURITY_TYPE_EAP_TLS, "", keyPass); + boolean hidden = !out.contains(keyPass); + boolean hasRedacted = out.contains(REDACTED); + assert hidden : "FAIL T5: fixed EAP-TLS path should not contain key_passwd"; + assert hasRedacted : "FAIL T5: [redacted] expected in EAP-TLS output"; + System.out.printf("T5 PASS: EAP key_passwd hidden=%b redacted=%b%n", hidden, hasRedacted); + pass++; + } + + // Test 6: OPEN network (no password) - no redacted marker, no password field + { + total++; + String out = printNetworkInfoFixed("PublicWifi", WIFI_SECURITY_TYPE_NONE, "", null); + boolean noPassword = !out.contains("password:"); + assert noPassword : "FAIL T6: OPEN network should not have password field"; + System.out.println("T6 PASS: OPEN network - no password field in output"); + pass++; + } + + // Test 7: SSID is still shown (not a secret) + { + total++; + String ssid = "MyHomeNetwork"; + String psk = "secret123"; + String out = printNetworkInfoFixed(ssid, WIFI_SECURITY_TYPE_PSK, psk, null); + assert out.contains(ssid) : "FAIL T7: SSID should still be visible"; + assert !out.contains(psk) : "FAIL T7: PSK should be redacted"; + assert out.contains(REDACTED) : "FAIL T7: [redacted] expected"; + System.out.println("T7 PASS: SSID visible, PSK redacted"); + pass++; + } + + // Test 8: WPA-PSK (legacy) also redacted + { + total++; + String psk = "OldSchoolPass"; + String out = printNetworkInfoFixed("LegacyAP", WIFI_SECURITY_TYPE_WPA_PSK, psk, null); + assert !out.contains(psk) : "FAIL T8: WPA-PSK password should be redacted"; + assert out.contains(REDACTED) : "FAIL T8: [redacted] expected for WPA-PSK"; + System.out.println("T8 PASS: WPA-PSK (legacy) password redacted"); + pass++; + } + + System.out.printf("%n%d/%d tests PASS%n", pass, total); + if (pass != total) { + System.exit(1); + } + } +} diff --git a/defects/zephyr-0002/patch/zephyr-0002.patch b/defects/zephyr-0002/patch/zephyr-0002.patch new file mode 100644 index 000000000..ad2838776 --- /dev/null +++ b/defects/zephyr-0002/patch/zephyr-0002.patch @@ -0,0 +1,36 @@ +--- a/subsys/net/l2/wifi/wifi_mgmt.c ++++ b/subsys/net/l2/wifi/wifi_mgmt.c +@@ -396,9 +396,14 @@ static int wifi_connect(uint64_t mgmt_request, struct net_if *iface, + # Defect ID: zephyr-0002 + # MOAD: 0004 + # Severity: HIGH + # CVE class: CWE-312 Cleartext Storage/Exposure of Sensitive Information + # + # Root cause: wifi_connect() in wifi_mgmt.c dumps the WiFi PSK and SAE + # password as raw hex via LOG_HEXDUMP_DBG on every connection attempt: + # + # line 396: LOG_HEXDUMP_DBG(params->ssid, params->ssid_length, "ssid"); + # line 397: LOG_HEXDUMP_DBG(params->psk, params->psk_length, "psk"); + # line 399: LOG_HEXDUMP_DBG(params->sae_password, params->sae_password_length, "sae"); + # + # LOG_HEXDUMP_DBG fires at LOG_LEVEL_DBG. When a developer or board BSP sets + # CONFIG_WIFI_LOG_LEVEL_DBG=y (common during WiFi bring-up and certification + # testing), the PSK is dumped to the Zephyr logging backend in hex. On boards + # with RTT, UART, or flash logging backends this exposes the network passphrase + # in cleartext to anyone with console or log access. + # + # The SSID is not a credential and is fine to log. The PSK and SAE password + # must not appear in logs at any log level. + # + # Fix: remove LOG_HEXDUMP_DBG for psk and sae_password. Log only the SSID, + # channel and security type (already logged by NET_DBG two lines below). + + LOG_HEXDUMP_DBG(params->ssid, params->ssid_length, "ssid"); +- LOG_HEXDUMP_DBG(params->psk, params->psk_length, "psk"); +- if (params->sae_password) { +- LOG_HEXDUMP_DBG(params->sae_password, params->sae_password_length, "sae"); +- } ++ /* PATCH zephyr-0002: do not log PSK or SAE password (CWE-312). */ ++ LOG_DBG("psk set: %s", params->psk_length > 0 ? "yes" : "no"); ++ LOG_DBG("sae_password set: %s", (params->sae_password && params->sae_password_length > 0) ? "yes" : "no"); + NET_DBG("ch %u sec %u", params->channel, params->security); diff --git a/defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.class b/defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.class new file mode 100644 index 000000000..5376bf877 Binary files /dev/null and b/defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.class differ diff --git a/defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.java b/defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.java new file mode 100644 index 000000000..e394c286d --- /dev/null +++ b/defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.java @@ -0,0 +1,184 @@ +import java.util.*; + +/** + * Unit test for zephyr-0002: wifi_mgmt.c wifi_connect() dumps WiFi PSK and + * SAE password as raw hex via LOG_HEXDUMP_DBG on every connection (CWE-312). + * + * Models the C pattern in wifi_connect(): + * + * LOG_HEXDUMP_DBG(params->ssid, params->ssid_length, "ssid"); + * LOG_HEXDUMP_DBG(params->psk, params->psk_length, "psk"); // DEFECT + * if (params->sae_password) { + * LOG_HEXDUMP_DBG(params->sae_password, params->sae_password_length, "sae"); // DEFECT + * } + * + * LOG_HEXDUMP_DBG fires at CONFIG_WIFI_LOG_LEVEL_DBG=y (set on most dev boards + * during bring-up). The PSK is printed as hex bytes to UART/RTT/flash log. + * + * Fix: remove the PSK and SAE hexdump lines. Replace with a boolean presence + * indicator ("psk set: yes/no") so developers can see a credential is set + * without exposing its value. + */ +public class ZephyrWifiMgmtPskLogTest { + + // --- Simulate the LOG_HEXDUMP_DBG output (hex bytes) --- + static String hexDump(String label, byte[] data) { + StringBuilder sb = new StringBuilder(label + ": "); + for (byte b : data) { + sb.append(String.format("%02x ", b)); + } + return sb.toString().trim(); + } + + // --- Defective wifi_connect log lines --- + static List wifiConnectLogDefective(String ssid, String psk, String saePassword) { + List lines = new ArrayList<>(); + // ssid is fine to log + lines.add(hexDump("ssid", ssid.getBytes())); + // DEFECT: PSK hex-dumped + if (psk != null && !psk.isEmpty()) { + lines.add(hexDump("psk", psk.getBytes())); + } + // DEFECT: SAE password hex-dumped + if (saePassword != null && !saePassword.isEmpty()) { + lines.add(hexDump("sae", saePassword.getBytes())); + } + return lines; + } + + // --- Fixed wifi_connect log lines --- + static List wifiConnectLogFixed(String ssid, String psk, String saePassword) { + List lines = new ArrayList<>(); + // SSID still logged + lines.add(hexDump("ssid", ssid.getBytes())); + // FIX: presence only, no value + lines.add("psk set: " + (psk != null && !psk.isEmpty() ? "yes" : "no")); + lines.add("sae_password set: " + (saePassword != null && !saePassword.isEmpty() ? "yes" : "no")); + return lines; + } + + // Helper: check if any log line contains the given string + static boolean logsContain(List lines, String needle) { + for (String line : lines) { + if (line.contains(needle)) return true; + } + return false; + } + + // Helper: check if any log line contains hex bytes of a string + static boolean logsContainHex(List lines, String secret) { + byte[] bytes = secret.getBytes(); + // Build hex representation of first 4 bytes as a search key + if (bytes.length == 0) return false; + String hexKey = String.format("%02x %02x", bytes[0], bytes[1 % bytes.length]); + for (String line : lines) { + if (line.contains(hexKey)) return true; + } + return false; + } + + public static void main(String[] args) { + System.out.println("=== zephyr-0002: wifi_mgmt.c PSK LOG_HEXDUMP_DBG CWE-312 ===\n"); + + int pass = 0, total = 0; + + // Test 1: defective path logs PSK hex bytes + { + total++; + String psk = "SecretWifi2024"; + List logs = wifiConnectLogDefective("MySSID", psk, null); + boolean containsPsk = logsContain(logs, "psk:"); + boolean hexPresent = logsContainHex(logs, psk); + assert containsPsk : "FAIL T1: expected psk hexdump line"; + assert hexPresent : "FAIL T1: expected PSK hex bytes in log"; + System.out.println("T1 CONFIRMED defective: PSK hex in log = " + hexPresent); + pass++; + } + + // Test 2: fixed path does not log PSK value + { + total++; + String psk = "SecretWifi2024"; + List logs = wifiConnectLogFixed("MySSID", psk, null); + boolean hexAbsent = !logsContainHex(logs, psk); + boolean hasPresence = logsContain(logs, "psk set: yes"); + assert hexAbsent : "FAIL T2: fixed path should not log PSK hex bytes"; + assert hasPresence : "FAIL T2: fixed path should show 'psk set: yes'"; + System.out.println("T2 PASS fixed: PSK hex absent=" + hexAbsent + " presence logged=" + hasPresence); + pass++; + } + + // Test 3: SAE password defective path leaks hex + { + total++; + String saePass = "WPA3Pass!@#"; + List logs = wifiConnectLogDefective("Office5G", null, saePass); + boolean hexPresent = logsContainHex(logs, saePass); + assert hexPresent : "FAIL T3: expected SAE hex bytes in defective log"; + System.out.println("T3 CONFIRMED defective: SAE hex in log = " + hexPresent); + pass++; + } + + // Test 4: SAE password fixed path does not leak + { + total++; + String saePass = "WPA3Pass!@#"; + List logs = wifiConnectLogFixed("Office5G", null, saePass); + boolean hexAbsent = !logsContainHex(logs, saePass); + boolean hasPresence = logsContain(logs, "sae_password set: yes"); + assert hexAbsent : "FAIL T4: fixed path should not log SAE hex bytes"; + assert hasPresence : "FAIL T4: fixed path should show sae_password presence"; + System.out.printf("T4 PASS: SAE hex absent=%b presence=%b%n", hexAbsent, hasPresence); + pass++; + } + + // Test 5: SSID is still logged in full (not a secret) + { + total++; + String ssid = "CorporateNet"; + List logs = wifiConnectLogFixed(ssid, "pass", null); + boolean ssidLogged = logsContain(logs, "ssid:"); + assert ssidLogged : "FAIL T5: SSID should still be logged"; + System.out.println("T5 PASS: SSID still logged"); + pass++; + } + + // Test 6: no PSK → presence indicator says "no" + { + total++; + List logs = wifiConnectLogFixed("OpenNet", null, null); + boolean noSet = logsContain(logs, "psk set: no"); + assert noSet : "FAIL T6: 'psk set: no' expected when no PSK"; + System.out.println("T6 PASS: 'psk set: no' for open network"); + pass++; + } + + // Test 7: PSK with common chars (verify hex check is working) + { + total++; + String psk = "abcdefgh"; + List defLogs = wifiConnectLogDefective("Net", psk, null); + List fixLogs = wifiConnectLogFixed("Net", psk, null); + assert logsContainHex(defLogs, psk) : "FAIL T7: hex detection sanity check failed"; + assert !logsContainHex(fixLogs, psk) : "FAIL T7: fixed path should not contain hex of PSK"; + System.out.println("T7 PASS: hex detection sanity check OK"); + pass++; + } + + // Test 8: short PSK (8 chars minimum) still redacted + { + total++; + String psk = "12345678"; + List logs = wifiConnectLogFixed("MinPsk", psk, null); + assert !logsContainHex(logs, psk) : "FAIL T8: short PSK should still be redacted"; + assert logsContain(logs, "psk set: yes") : "FAIL T8: 'psk set: yes' expected"; + System.out.println("T8 PASS: short PSK (8 chars) redacted"); + pass++; + } + + System.out.printf("%n%d/%d tests PASS%n", pass, total); + if (pass != total) { + System.exit(1); + } + } +} diff --git a/defects/zephyr/CLEAN.md b/defects/zephyr/CLEAN.md new file mode 100644 index 000000000..a72d3b7e8 --- /dev/null +++ b/defects/zephyr/CLEAN.md @@ -0,0 +1,50 @@ +# Zephyr RTOS — MOAD scan summary + +**Target:** zephyr (https://github.com/zephyrproject-rtos/zephyr) +**Scan date:** 2026-03-31 +**Commit:** depth=1 HEAD + +## MOAD-0001 (CWE-407) — CLEAN (kernel + net core) + +No O(N²) list-contains-inside-loop in hot paths found. + +- Scheduler uses priority bitmask (`_prio_run_bitmask`) + priority-keyed wait + queues — O(1) ready task selection. +- `net_if_ipv4_addr_lookup_raw` iterates interfaces×addresses — O(I×A) called + only on control-path (bind, autoconf, ARP), not per-packet. +- `nbr_lookup` (IPv6 neighbor cache) is O(N_neighbors) — CONFIG_NET_IPV6_MAX_NEIGHBORS + defaults to 8, effectively O(1) in practice; not nested inside another loop. +- `pkt_filter/base.c` `evaluate()` does O(R×T) rule+test scan per packet — + R and T are compile-time bounded small constants, not dynamically growing lists. +- `sys_slist_find` in bluetooth host is used as a registration guard (once per + callback register), not per-event — O(C) not O(C²). +- Net management event dispatch (`net_mgmt.c`) is a single-pass O(C) callback + scan per event — no inner list scan. + +## MOAD-0002 (Intertangle) — CLEAN + +Net subsystem uses a clean layered architecture: L2 (wifi/ethernet) registers +with net_if via a fixed vtable (`net_l2_api`). IPv4/IPv6 are accessed through +`iface->config.ip.ipv4/ipv6`. No shared mutable global god object coupling +independent subsystems was found. + +## MOAD-0003 (Leaked Context) — CLEAN + +`Z_THREAD_LOCAL z_errno_var` is per-thread errno — correct and standard. +`k_thread_custom_data` is a general-purpose user slot; no pattern of +request-scoped identity stored in it and then leaked to a subsequent task was +found. Zephyr does not have a ScopedValue-equivalent API because it targets +bare-metal RTOS contexts without user-space green threads. + +## MOAD-0004 (CWE-312) — 2 DEFECTS (see zephyr-0001, zephyr-0002) + +- **zephyr-0001**: `wifi_credentials_shell.c` `print_network_info()` prints PSK + and EAP-TLS key_passwd verbatim via shell — patched. +- **zephyr-0002**: `wifi_mgmt.c` `wifi_connect()` dumps PSK and SAE password as + hex via LOG_HEXDUMP_DBG — patched. + +## MOAD-0005 (Thundering Herd) — CLEAN + +Memory slabs (`kernel/mem_slab.c`) use `k_spin_lock` around all alloc/free +operations — no unsynchronized get+null+alloc+put. Kernel object pools are +protected by ISR-safe spinlocks. No concurrent cache race pattern found.