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:
parent
81bef63b2e
commit
1e10775b90
21 changed files with 920 additions and 0 deletions
45
defects/freertos/CLEAN.md
Normal file
45
defects/freertos/CLEAN.md
Normal 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.
|
||||
7
defects/jitsi-videobridge/MOAD-0002-CLEAN.md
Normal file
7
defects/jitsi-videobridge/MOAD-0002-CLEAN.md
Normal 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
|
||||
7
defects/jitsi-videobridge/MOAD-0003-CLEAN.md
Normal file
7
defects/jitsi-videobridge/MOAD-0003-CLEAN.md
Normal 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
|
||||
7
defects/jitsi-videobridge/MOAD-0004-CLEAN.md
Normal file
7
defects/jitsi-videobridge/MOAD-0004-CLEAN.md
Normal 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
|
||||
8
defects/jitsi-videobridge/MOAD-0005-CLEAN.md
Normal file
8
defects/jitsi-videobridge/MOAD-0005-CLEAN.md
Normal 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
|
||||
7
defects/woodpecker-0001/MOAD-0002-CLEAN.md
Normal file
7
defects/woodpecker-0001/MOAD-0002-CLEAN.md
Normal 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
|
||||
7
defects/woodpecker-0001/MOAD-0003-CLEAN.md
Normal file
7
defects/woodpecker-0001/MOAD-0003-CLEAN.md
Normal 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)
|
||||
8
defects/woodpecker-0001/MOAD-0005-CLEAN.md
Normal file
8
defects/woodpecker-0001/MOAD-0005-CLEAN.md
Normal 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
|
||||
|
|
@ -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
|
||||
-}
|
||||
3
defects/woodpecker-0001/test/go.mod
Normal file
3
defects/woodpecker-0001/test/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module woodpecker_test
|
||||
|
||||
go 1.21
|
||||
165
defects/woodpecker-0001/test/woodpecker_step_builder_test.go
Normal file
165
defects/woodpecker-0001/test/woodpecker_step_builder_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
3
defects/woodpecker-0002/test/go.mod
Normal file
3
defects/woodpecker-0002/test/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module woodpecker_token_test
|
||||
|
||||
go 1.21
|
||||
69
defects/woodpecker-0002/test/woodpecker_token_cwe312_test.go
Normal file
69
defects/woodpecker-0002/test/woodpecker_token_cwe312_test.go
Normal 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")
|
||||
}
|
||||
58
defects/zephyr-0001/patch/zephyr-0001.patch
Normal file
58
defects/zephyr-0001/patch/zephyr-0001.patch
Normal 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);
|
||||
}
|
||||
BIN
defects/zephyr-0001/test/ZephyrWifiCredShellTest.class
Normal file
BIN
defects/zephyr-0001/test/ZephyrWifiCredShellTest.class
Normal file
Binary file not shown.
187
defects/zephyr-0001/test/ZephyrWifiCredShellTest.java
Normal file
187
defects/zephyr-0001/test/ZephyrWifiCredShellTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
defects/zephyr-0002/patch/zephyr-0002.patch
Normal file
36
defects/zephyr-0002/patch/zephyr-0002.patch
Normal 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);
|
||||
BIN
defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.class
Normal file
BIN
defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.class
Normal file
Binary file not shown.
184
defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.java
Normal file
184
defects/zephyr-0002/test/ZephyrWifiMgmtPskLogTest.java
Normal 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
50
defects/zephyr/CLEAN.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue