jitsi-videobridge: 3 CWE-407 defects, MOADs 0002-0005 CLEAN; woodpecker: 1 CWE-407 + 1 CWE-312, MOADs 0002/0003/0005 CLEAN

jitsi-videobridge (Kotlin/Java video conferencing bridge):
- 0001: Prioritize.kt selectedSourceNames.contains()+indexOf() inside forEach over conferenceSources, O(C*S)
- 0002: BandwidthAllocator.kt selectedSources getter List.contains() dedup inside forEach, O(S^2)
- 0003: ConferenceSpeechActivity.java endpointsChanged() ArrayList.contains() in removeIf+for loop, O(E^2)
  Fix: HashSet for O(1) membership; pre-built index map for indexOf
  Unit test: 4/4 PASS, 19-35x op-count reduction at N=200

woodpecker-0001 (Go CI/CD pipeline step builder):
- filterItemsWithMissingDependencies() calls containsItemWithName() (O(N) linear scan) inside
  two nested loops over items and deps: O(N*D*N) = O(N^2)
  Fix: pre-build name-set map for O(1) lookup, O(N) total
  Unit test: 3/3 PASS, 20x op-count reduction at N=100

woodpecker-0002 (CWE-312 credential logging):
- shared/token/token.go ParseRequest() logs raw Authorization header value at Trace level:
  log.Trace().Msgf("token.ParseRequest: found token in header: %s", token)
  Exposes full Bearer JWT token in application logs
  Fix: log only that header was found, not its value
  Unit test: 3/3 PASS
This commit is contained in:
russell@unturf.com 2026-03-31 20:18:44 -04:00
parent 81bef63b2e
commit 1e10775b90
21 changed files with 920 additions and 0 deletions

45
defects/freertos/CLEAN.md Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
module woodpecker_test
go 1.21

View file

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

View file

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

View file

@ -0,0 +1,3 @@
module woodpecker_token_test
go 1.21

View file

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

View file

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

Binary file not shown.

View file

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

View file

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

View file

@ -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<String> wifiConnectLogDefective(String ssid, String psk, String saePassword) {
List<String> 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<String> wifiConnectLogFixed(String ssid, String psk, String saePassword) {
List<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> defLogs = wifiConnectLogDefective("Net", psk, null);
List<String> 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<String> 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);
}
}
}

50
defects/zephyr/CLEAN.md Normal file
View file

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