esp-idf: 2 CWE-312 defects (WiFi PSK + HTTP auth password logged); drone: CWE-407 pubsub O(S*T) + MOAD-0005 thundering herd; MOADs 0002/0003 CLEAN

This commit is contained in:
russell@unturf.com 2026-03-31 20:30:06 -04:00
parent ad90dddbcc
commit bc17a5e700
10 changed files with 883 additions and 0 deletions

View file

@ -0,0 +1,74 @@
--- a/pubsub/inmem.go
+++ b/pubsub/inmem.go
@@ -14,7 +14,6 @@ import (
"context"
"errors"
"sync"
"time"
"github.com/rs/zerolog/log"
- "golang.org/x/exp/slices"
)
@@ -91,7 +90,8 @@ func (r *InMemory) Publish(ctx context.Context, topic string, payload []byte, op
topic = formatTopic(pubConfig.app, pubConfig.namespace, topic)
wg := sync.WaitGroup{}
for _, sub := range r.registry {
- if slices.Contains(sub.topics, topic) && !sub.isClosed() {
+ if sub.hasTopic(topic) && !sub.isClosed() {
wg.Add(1)
go func(subscriber *inMemorySubscriber) {
defer wg.Done()
@@ -128,9 +128,9 @@ func (s *inMemorySubscriber) Subscribe(_ context.Context, topics ...string) erro
defer s.mutex.RUnlock()
topics = s.formatTopics(topics...)
for _, ch := range topics {
- if slices.Contains(s.topics, ch) {
- continue
- }
- s.topics = append(s.topics, ch)
+ if _, ok := s.topicSet[ch]; !ok {
+ s.topicSet[ch] = struct{}{}
+ s.topics = append(s.topics, ch)
+ }
}
return nil
}
@@ -140,9 +140,9 @@ func (s *inMemorySubscriber) Unsubscribe(_ context.Context, topics ...string) er
defer s.mutex.RUnlock()
topics = s.formatTopics(topics...)
for _, ch := range topics {
- if slices.Contains(s.topics, ch) {
- s.topics[i] = s.topics[len(s.topics)-1]
- s.topics = s.topics[:len(s.topics)-1]
- }
+ if _, ok := s.topicSet[ch]; ok {
+ delete(s.topicSet, ch)
+ // rebuild slice from set
+ s.topics = s.topics[:0]
+ for t := range s.topicSet {
+ s.topics = append(s.topics, t)
+ }
+ }
}
return nil
}
+func (s *inMemorySubscriber) hasTopic(topic string) bool {
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+ _, ok := s.topicSet[topic]
+ return ok
+}
// struct change: add topicSet map alongside topics slice
type inMemorySubscriber struct {
config *SubscribeConfig
handler func([]byte) error
channel chan []byte
once sync.Once
mutex sync.RWMutex
topics []string
+ topicSet map[string]struct{}
closed bool
}

View file

@ -0,0 +1,179 @@
import java.util.*;
/**
* Test for drone-0001: CWE-407 O(S*T) linear topic scan in InMemory pubsub Publish.
*
* Location: pubsub/inmem.go InMemory.Publish() and inMemorySubscriber.Subscribe()
*
* Defect: for each subscriber (S), slices.Contains(sub.topics, topic) performs
* an O(T) linear scan over topics. With S subscribers each holding T topics, one
* Publish call costs O(S*T). Subscribe() and Unsubscribe() also use
* slices.Contains inside their own loops.
*
* In a busy Drone CI instance running many concurrent SSE streams and
* pipeline-event notifications this creates quadratic work per event published.
*
* Fix: maintain a map[string]struct{} topicSet alongside the topics slice.
* hasTopic() becomes O(1) map lookup. Publish drops from O(S*T) to O(S).
*
* Speedup at S=500 subscribers, T=20 topics: 500*20=10000 ops vs 500 ops = 20x.
*
* Compile and run (no build tool required):
* javac defects/drone-0001/test/Drone0001Test.java -d /tmp/drone-0001
* java -cp /tmp/drone-0001 Drone0001Test
*/
public class Drone0001Test {
private static int passed = 0;
private static int failed = 0;
// --- Defective subscriber: slice-based topic membership ---
static class SubscriberDefective {
final List<String> topics = new ArrayList<>();
void addTopic(String t) {
if (!topics.contains(t)) topics.add(t); // O(T) scan
}
boolean hasTopic(String t) {
return topics.contains(t); // O(T) scan defect site
}
}
// --- Fixed subscriber: map-based topic membership ---
static class SubscriberFixed {
final Set<String> topicSet = new HashSet<>();
void addTopic(String t) {
topicSet.add(t); // O(1)
}
boolean hasTopic(String t) {
return topicSet.contains(t); // O(1) fix
}
}
/**
* Counts total element comparisons when Publish scans S subscribers,
* each with T topics, and the publish topic is NOT present (worst case).
*/
static long publishCostDefective(int S, int T, String publishTopic) {
List<SubscriberDefective> subs = new ArrayList<>();
for (int s = 0; s < S; s++) {
SubscriberDefective sub = new SubscriberDefective();
for (int t = 0; t < T; t++) sub.addTopic("t" + s + "." + t);
subs.add(sub);
}
long ops = 0;
for (SubscriberDefective sub : subs) {
for (int i = 0; i < sub.topics.size(); i++) {
ops++;
if (sub.topics.get(i).equals(publishTopic)) break;
}
}
return ops;
}
static long publishCostFixed(int S, int T, String publishTopic) {
List<SubscriberFixed> subs = new ArrayList<>();
for (int s = 0; s < S; s++) {
SubscriberFixed sub = new SubscriberFixed();
for (int t = 0; t < T; t++) sub.addTopic("t" + s + "." + t);
subs.add(sub);
}
// one O(1) hash lookup per subscriber
long ops = 0;
for (SubscriberFixed sub : subs) {
sub.hasTopic(publishTopic);
ops++;
}
return ops;
}
// --- Tests ---
static void testDefectiveQuadraticCost() {
long cost100 = publishCostDefective(100, 20, "absent");
long cost200 = publishCostDefective(200, 20, "absent");
// doubling S doubles ops (O(S*T) with fixed T)
check("defective cost at S=100,T=20 is exactly 2000",
cost100 == 100L * 20);
check("defective cost doubles when S doubles (O(S*T))",
cost200 == cost100 * 2);
}
static void testFixedLinearCost() {
long cost100 = publishCostFixed(100, 20, "absent");
long cost200 = publishCostFixed(200, 20, "absent");
check("fixed cost at S=100 is exactly 100 (one lookup per subscriber)",
cost100 == 100);
check("fixed cost at S=200 is exactly 200",
cost200 == 200);
}
static void testSpeedupRatioAtScale() {
int S = 500, T = 20;
long defCost = publishCostDefective(S, T, "none");
long fixCost = publishCostFixed(S, T, "none");
double ratio = (double) defCost / fixCost;
check("speedup at S=500,T=20 is at least 15x (expect ~20x)",
ratio >= 15.0);
System.out.printf(" drone-0001 publish cost: defective=%d fixed=%d ratio=%.1fx%n",
defCost, fixCost, ratio);
}
static void testFixedSubscriberDeduplicates() {
SubscriberFixed sub = new SubscriberFixed();
sub.addTopic("events:pipeline");
sub.addTopic("events:pipeline"); // duplicate
sub.addTopic("events:repo");
check("set-based subscriber deduplicates topics",
sub.topicSet.size() == 2);
}
static void testFixedHasTopicCorrect() {
SubscriberFixed sub = new SubscriberFixed();
sub.addTopic("events:pipeline:exec");
check("hasTopic returns true for subscribed topic",
sub.hasTopic("events:pipeline:exec"));
check("hasTopic returns false for unsubscribed topic",
!sub.hasTopic("events:repo:push"));
}
static void testDefectiveHasTopicCorrect() {
SubscriberDefective sub = new SubscriberDefective();
sub.addTopic("events:pipeline:exec");
check("defective hasTopic returns true for subscribed topic",
sub.hasTopic("events:pipeline:exec"));
check("defective hasTopic returns false for unsubscribed topic",
!sub.hasTopic("events:repo:push"));
}
// --- Harness ---
static void check(String desc, boolean cond) {
if (cond) {
System.out.println(" PASS: " + desc);
passed++;
} else {
System.out.println(" FAIL: " + desc);
failed++;
}
}
public static void main(String[] args) {
System.out.println("=== Drone0001Test (CWE-407 pubsub Publish O(S*T) topic scan) ===\n");
testDefectiveQuadraticCost();
testFixedLinearCost();
testSpeedupRatioAtScale();
testFixedSubscriberDeduplicates();
testFixedHasTopicCorrect();
testDefectiveHasTopicCorrect();
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
if (failed > 0) System.exit(1);
}
}

View file

@ -0,0 +1,81 @@
--- a/cache/ttl_cache.go
+++ b/cache/ttl_cache.go
@@ -14,6 +14,7 @@ import (
"context"
"fmt"
"slices"
+ "sync"
"sync/atomic"
"time"
@@ -25,6 +26,7 @@ type TTLCache[K comparable, V any] struct {
mx sync.RWMutex
cache map[K]cacheEntry[V]
purgeStop chan struct{}
+ group singleflightGroup[K, V]
getter Getter[K, V]
maxAge time.Duration
countHit atomic.Int64
@@ -200,13 +202,27 @@ func (c *TTLCache[K, V]) Get(ctx context.Context, key K) (V, error) {
item, ok := c.fetch(key, now)
if ok {
return item, nil
}
- item, err := c.getter.Find(ctx, key)
+ // Use singleflight to prevent thundering herd: concurrent goroutines that
+ // miss the cache for the same key share a single backend fetch instead of
+ // each issuing their own database call.
+ item, err, _ := c.group.Do(key, func() (V, error) {
+ return c.getter.Find(ctx, key)
+ })
if err != nil {
return nothing, fmt.Errorf("cache: failed to find one: %w", err)
}
c.mx.Lock()
c.cache[key] = cacheEntry[V]{
added: now,
data: item,
}
c.mx.Unlock()
return item, nil
}
+// singleflightGroup is a minimal generic singleflight implementation.
+type singleflightGroup[K comparable, V any] struct {
+ mu sync.Mutex
+ m map[K]*call[V]
+}
+
+type call[V any] struct {
+ wg sync.WaitGroup
+ val V
+ err error
+}
+
+func (g *singleflightGroup[K, V]) Do(key K, fn func() (V, error)) (V, error, bool) {
+ g.mu.Lock()
+ if g.m == nil {
+ g.m = make(map[K]*call[V])
+ }
+ if c, ok := g.m[key]; ok {
+ g.mu.Unlock()
+ c.wg.Wait()
+ return c.val, c.err, true
+ }
+ c := &call[V]{}
+ c.wg.Add(1)
+ g.m[key] = c
+ g.mu.Unlock()
+
+ c.val, c.err = fn()
+ c.wg.Done()
+
+ g.mu.Lock()
+ delete(g.m, key)
+ g.mu.Unlock()
+
+ return c.val, c.err, false
+}

View file

@ -0,0 +1,200 @@
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/**
* Test for drone-0002: MOAD-0005 Thundering Herd in cache/ttl_cache.go TTLCache.Get().
*
* Location: cache/ttl_cache.go TTLCache.Get()
*
* Defect pattern (CWE-362 cache stampede):
* 1. fetch(key) acquires RLock, returns cache miss
* 2. getter.Find(ctx, key) called with NO lock held
* 3. mx.Lock(); cache[key] = item; mx.Unlock()
*
* Under concurrent load: C goroutines all see a cache miss simultaneously for
* the same key, all call getter.Find() independently, all issue C database
* round-trips for the same record. A thundering herd of DB queries results.
*
* Fix: wrap getter.Find() in a singleflight group keyed by the cache key.
* Only the first goroutine issues the DB call; all others share its result.
* Backend calls drop from C to 1 for any C concurrent misses on the same key.
*
* Compile and run (no build tool required):
* javac defects/drone-0002/test/Drone0002Test.java -d /tmp/drone-0002
* java -cp /tmp/drone-0002 Drone0002Test
*/
public class Drone0002Test {
private static int passed = 0;
private static int failed = 0;
// --- Fake backend store with call counter ---
static class FakeStore {
final AtomicInteger callCount = new AtomicInteger(0);
final long latencyMs;
FakeStore(long latencyMs) { this.latencyMs = latencyMs; }
String find(String key) throws InterruptedException {
callCount.incrementAndGet();
Thread.sleep(latencyMs);
return "value-for-" + key;
}
}
// --- Defective cache: no singleflight ---
static class CacheDefective {
final Map<String, String> cache = new ConcurrentHashMap<>();
final FakeStore store;
CacheDefective(FakeStore s) { this.store = s; }
String get(String key) throws Exception {
String v = cache.get(key);
if (v != null) return v;
// thundering herd: multiple threads all reach here simultaneously
v = store.find(key);
cache.put(key, v);
return v;
}
}
// --- Fixed cache: singleflight coalesces concurrent misses ---
static class CacheFixed {
final Map<String, String> cache = new ConcurrentHashMap<>();
final FakeStore store;
final ConcurrentHashMap<String, CompletableFuture<String>> inflight = new ConcurrentHashMap<>();
CacheFixed(FakeStore s) { this.store = s; }
String get(String key) throws Exception {
String v = cache.get(key);
if (v != null) return v;
CompletableFuture<String> mine = new CompletableFuture<>();
CompletableFuture<String> existing = inflight.putIfAbsent(key, mine);
if (existing != null) return existing.get(); // wait for in-flight call
try {
v = store.find(key);
cache.put(key, v);
mine.complete(v);
return v;
} catch (Exception e) {
mine.completeExceptionally(e);
throw e;
} finally {
inflight.remove(key);
}
}
}
// --- Helpers ---
static int countBackendCalls(boolean useFixed, int concurrent, long latencyMs)
throws Exception {
FakeStore store = new FakeStore(latencyMs);
Object cache = useFixed ? new CacheFixed(store) : new CacheDefective(store);
CountDownLatch ready = new CountDownLatch(concurrent);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(concurrent);
ExecutorService pool = Executors.newFixedThreadPool(concurrent);
for (int i = 0; i < concurrent; i++) {
pool.submit(() -> {
ready.countDown();
try {
start.await();
if (useFixed) ((CacheFixed) cache).get("mykey");
else ((CacheDefective) cache).get("mykey");
} catch (Exception ignored) {
} finally { done.countDown(); }
});
}
ready.await(5, TimeUnit.SECONDS); // all threads waiting at start gate
start.countDown(); // release simultaneously
done.await(10, TimeUnit.SECONDS);
pool.shutdown();
return store.callCount.get();
}
// --- Tests ---
static void testDefectiveTriggersManyBackendCalls() throws Exception {
int concurrent = 20;
int calls = countBackendCalls(false, concurrent, 30);
check("defective cache triggers >1 backend calls (thundering herd confirmed)",
calls > 1);
System.out.printf(" drone-0002 defective backend calls at C=%d: %d (expect ~%d)%n",
concurrent, calls, concurrent);
}
static void testFixedTriggersExactlyOneBackendCall() throws Exception {
int concurrent = 20;
int calls = countBackendCalls(true, concurrent, 30);
check("fixed cache triggers exactly 1 backend call (singleflight working)",
calls == 1);
System.out.printf(" drone-0002 fixed backend calls at C=%d: %d (expect 1)%n",
concurrent, calls);
}
static void testFixedReturnCorrectValue() throws Exception {
FakeStore store = new FakeStore(5);
CacheFixed cache = new CacheFixed(store);
String result = cache.get("mykey");
check("fixed cache returns correct value", "value-for-mykey".equals(result));
}
static void testFixedSecondCallHitsCache() throws Exception {
FakeStore store = new FakeStore(0);
CacheFixed cache = new CacheFixed(store);
cache.get("k1");
int afterFirst = store.callCount.get();
cache.get("k1"); // second call must hit cache
int afterSecond = store.callCount.get();
check("second Get on same key hits cache (no backend call)",
afterFirst == afterSecond);
}
static void testSpeedupRatioAtConcurrency20() throws Exception {
int concurrent = 20;
int defCalls = countBackendCalls(false, concurrent, 15);
int fixCalls = countBackendCalls(true, concurrent, 15);
double ratio = (double) defCalls / Math.max(1, fixCalls);
check("fixed version reduces backend calls by at least 10x at C=20",
ratio >= 10.0);
System.out.printf(" drone-0002 thundering herd ratio at C=%d: %.1fx fewer backend calls%n",
concurrent, ratio);
}
// --- Harness ---
static void check(String desc, boolean cond) {
if (cond) {
System.out.println(" PASS: " + desc);
passed++;
} else {
System.out.println(" FAIL: " + desc);
failed++;
}
}
public static void main(String[] args) throws Exception {
System.out.println("=== Drone0002Test (MOAD-0005 Thundering Herd in TTLCache.Get) ===\n");
testDefectiveTriggersManyBackendCalls();
testFixedTriggersExactlyOneBackendCall();
testFixedReturnCorrectValue();
testFixedSecondCallHitsCache();
testSpeedupRatioAtConcurrency20();
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
if (failed > 0) System.exit(1);
}
}

27
defects/drone/SCAN.md Normal file
View file

@ -0,0 +1,27 @@
## Drone CI (Harness Gitness) 5-MOAD Scan — 2026-03-31
Target: https://github.com/harness/drone (depth=1)
### MOAD-0001 (CWE-407) — 1 DEFECT FOUND
See drone-0001. pubsub/inmem.go Publish() calls slices.Contains(sub.topics, topic)
for every subscriber — O(S*T) per Publish call.
### MOAD-0002 (Intertangle) — CLEAN
Manager struct aggregates stores via dependency injection; each store interface is
independently swappable. No shared mutable global state coupling pipeline execution
to unrelated subsystems. Wire-based DI separates concerns cleanly.
### MOAD-0003 (Leaked Context) — CLEAN
app/api/request/context.go uses context.Context (standard Go pattern for
per-request identity). No goroutine-local or thread-local identity leakage found.
Pattern matches Kubernetes apiserver context design.
### MOAD-0004 (CWE-312) — CLEAN
Searched all log calls for token/secret/password/webhook literals. No credential
values passed to zerolog log lines. Secret service logs error conditions only,
never values. Webhook tokens are stored encrypted in DB, not logged.
### MOAD-0005 (Thundering Herd) — 1 DEFECT FOUND
See drone-0002. cache/ttl_cache.go TTLCache.Get() has classic cache stampede:
fetch (RLock miss) then getter.Find() with no lock, then write. Multiple goroutines
can all miss and all call DB simultaneously.

View file

@ -0,0 +1,9 @@
--- a/components/esp_wifi/src/smartconfig.c
+++ b/components/esp_wifi/src/smartconfig.c
@@ -32,7 +32,6 @@ static void handler_got_ssid_passwd(void *arg, esp_event_base_t base, int32_t e
memcpy(password, evt->password, sizeof(evt->password));
memcpy(cellphone_ip, evt->cellphone_ip, sizeof(evt->cellphone_ip));
ESP_LOGD(TAG, "SSID:%s", ssid);
- ESP_LOGD(TAG, "PASSWORD:%s", password);
ESP_LOGD(TAG, "Phone ip: %d.%d.%d.%d", cellphone_ip[0], cellphone_ip[1], cellphone_ip[2], cellphone_ip[3]);

View file

@ -0,0 +1,131 @@
import java.util.ArrayList;
import java.util.List;
/**
* Test for esp-idf-0001: CWE-312 WiFi password logged verbatim via ESP_LOGD
* in components/esp_wifi/src/smartconfig.c handler_got_ssid_passwd().
*
* Pattern:
* ESP_LOGD(TAG, "PASSWORD:%s", password)
*
* When debug logging is enabled the WiFi PSK (pre-shared key) is emitted
* verbatim to serial/JTAG output. Any listener on those interfaces obtains
* the plaintext credential without any further effort.
*
* Fix: remove the ESP_LOGD("PASSWORD:%s", ...) line. SSID logging (non-secret)
* is acceptable; PSK logging is not.
*
* Compile and run (no build tool required):
* javac defects/esp-idf-0001/test/EspIdf0001Test.java -d /tmp/esp-idf-0001
* java -cp /tmp/esp-idf-0001 EspIdf0001Test
*/
public class EspIdf0001Test {
private static int passed = 0;
private static int failed = 0;
// --- Simulated log collector ---
static class LogCollector {
private final List<String> lines = new ArrayList<>();
void logd(String tag, String fmt, Object... args) {
lines.add(String.format(fmt, args));
}
boolean containsText(String text) {
for (String line : lines) {
if (line.contains(text)) return true;
}
return false;
}
int size() { return lines.size(); }
}
// --- Defective: logs password in DEBUG output ---
static void handleGotSsidPasswdDefective(String ssid, String password, LogCollector log) {
log.logd("smartconfig", "SSID:%s", ssid);
log.logd("smartconfig", "PASSWORD:%s", password); // CWE-312 site
log.logd("smartconfig", "Phone ip: 192.168.1.1");
}
// --- Fixed: password line removed ---
static void handleGotSsidPasswdFixed(String ssid, String password, LogCollector log) {
log.logd("smartconfig", "SSID:%s", ssid);
// PASSWORD log line removed fix for esp-idf-0001
log.logd("smartconfig", "Phone ip: 192.168.1.1");
}
// --- Tests ---
static void testDefectiveLogsPassword() {
LogCollector log = new LogCollector();
String password = "MySecretWiFiPSK123";
handleGotSsidPasswdDefective("MyNet", password, log);
check("defective must log the password (confirms CWE-312 site is present)",
log.containsText(password));
}
static void testFixedDoesNotLogPassword() {
LogCollector log = new LogCollector();
String password = "MySecretWiFiPSK123";
handleGotSsidPasswdFixed("MyNet", password, log);
check("fixed must NOT log the WiFi PSK in any log line",
!log.containsText(password));
}
static void testFixedStillLogsSsid() {
LogCollector log = new LogCollector();
handleGotSsidPasswdFixed("HomeNetwork_5GHz", "secret", log);
check("SSID (non-sensitive) must still appear in logs after fix",
log.containsText("HomeNetwork_5GHz"));
}
static void testFixedHasFewerLines() {
LogCollector defLog = new LogCollector();
LogCollector fixLog = new LogCollector();
handleGotSsidPasswdDefective("net", "pw", defLog);
handleGotSsidPasswdFixed("net", "pw", fixLog);
check("fixed version produces fewer log lines than defective",
fixLog.size() < defLog.size());
}
static void testPasswordNotLeakedAcrossMultipleCalls() {
// Simulate multiple smartconfig events none should leak password
String[] passwords = {"PSK-alpha", "PSK-beta", "PSK-gamma"};
for (String pw : passwords) {
LogCollector log = new LogCollector();
handleGotSsidPasswdFixed("net", pw, log);
check("password '" + pw + "' must not appear in any log line",
!log.containsText(pw));
}
}
// --- Harness ---
static void check(String desc, boolean cond) {
if (cond) {
System.out.println(" PASS: " + desc);
passed++;
} else {
System.out.println(" FAIL: " + desc);
failed++;
}
}
public static void main(String[] args) {
System.out.println("=== EspIdf0001Test (CWE-312 WiFi PSK logged) ===\n");
testDefectiveLogsPassword();
testFixedDoesNotLogPassword();
testFixedStillLogsSsid();
testFixedHasFewerLines();
testPasswordNotLeakedAcrossMultipleCalls();
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
if (failed > 0) System.exit(1);
}
}

View file

@ -0,0 +1,8 @@
--- a/components/esp_http_client/lib/http_auth.c
+++ b/components/esp_http_client/lib/http_auth.c
@@ -155,7 +155,6 @@ char *http_auth_digest(const char *username, const char *password, esp_http_auth
if (digest_func(ha1, "%s:%s:%s", username, auth_data->realm, password) <= 0) {
goto _digest_exit;
}
- ESP_LOGD(TAG, "%s %s %s %s", "Digest", username, auth_data->realm, password);

View file

@ -0,0 +1,146 @@
import java.util.ArrayList;
import java.util.List;
/**
* Test for esp-idf-0002: CWE-312 HTTP Digest auth password logged verbatim
* in components/esp_http_client/lib/http_auth.c http_auth_digest().
*
* Pattern:
* ESP_LOGD(TAG, "%s %s %s %s", "Digest", username, auth_data->realm, password)
*
* During HTTP Digest authentication setup the plaintext password is emitted
* to serial/JTAG when debug logging is active. Unlike Basic Auth the password
* should never leave the chip except as part of the one-way digest hash.
*
* Fix: remove the offending ESP_LOGD line. Logging the digest hash result
* (ha1/response) would be safe; logging the raw password is not.
*
* Compile and run (no build tool required):
* javac defects/esp-idf-0002/test/EspIdf0002Test.java -d /tmp/esp-idf-0002
* java -cp /tmp/esp-idf-0002 EspIdf0002Test
*/
public class EspIdf0002Test {
private static int passed = 0;
private static int failed = 0;
// --- Simulated log collector ---
static class LogCollector {
private final List<String> lines = new ArrayList<>();
void logd(String tag, String fmt, Object... args) {
lines.add(String.format(fmt, args));
}
boolean containsText(String text) {
for (String line : lines) {
if (line.contains(text)) return true;
}
return false;
}
int size() { return lines.size(); }
}
/** Trivial stand-in for MD5 hex — actual hash value irrelevant for this test. */
static String mockMd5(String input) {
return Integer.toHexString(input.hashCode() & 0x7fffffff);
}
// --- Defective: logs username + realm + password ---
static String computeDigestDefective(
String username, String realm, String password, LogCollector log) {
String ha1 = mockMd5(username + ":" + realm + ":" + password);
log.logd("HTTP_AUTH", "%s %s %s %s", "Digest", username, realm, password); // CWE-312
String ha2 = mockMd5("GET:/api/data");
return mockMd5(ha1 + ":nonce123:" + ha2);
}
// --- Fixed: no password in any log statement ---
static String computeDigestFixed(
String username, String realm, String password, LogCollector log) {
String ha1 = mockMd5(username + ":" + realm + ":" + password);
// ESP_LOGD with password removed fix for esp-idf-0002
String ha2 = mockMd5("GET:/api/data");
return mockMd5(ha1 + ":nonce123:" + ha2);
}
// --- Tests ---
static void testDefectiveLogsPassword() {
LogCollector log = new LogCollector();
computeDigestDefective("alice", "testrealm@host.com", "hunter2", log);
check("defective must log the password (confirms CWE-312 site is present)",
log.containsText("hunter2"));
}
static void testFixedDoesNotLogPassword() {
LogCollector log = new LogCollector();
computeDigestFixed("alice", "testrealm@host.com", "hunter2", log);
check("fixed must NOT log the HTTP auth password",
!log.containsText("hunter2"));
}
static void testDefectiveAlsoExposesUsername() {
LogCollector log = new LogCollector();
computeDigestDefective("alice", "testrealm@host.com", "hunter2", log);
// The combined log line is the real risk (username + realm + password together)
check("defective log line includes username (combined exposure risk)",
log.containsText("alice"));
}
static void testFixedProducesNoLogLines() {
LogCollector log = new LogCollector();
computeDigestFixed("alice", "testrealm@host.com", "hunter2", log);
check("fixed digest computation emits no log lines",
log.size() == 0);
}
static void testBothReturnSameDigest() {
LogCollector log1 = new LogCollector();
LogCollector log2 = new LogCollector();
String d1 = computeDigestDefective("bob", "realm", "s3cr3t", log1);
String d2 = computeDigestFixed("bob", "realm", "s3cr3t", log2);
check("fixed and defective produce identical digest response (fix is behaviour-neutral)",
d1.equals(d2));
}
static void testPasswordNotLeakedForMultipleRealms() {
String[] realms = {"api.example.com", "admin.local", "iot-gateway"};
for (String realm : realms) {
LogCollector log = new LogCollector();
computeDigestFixed("user", realm, "TopSecretPW", log);
check("password not logged for realm '" + realm + "'",
!log.containsText("TopSecretPW"));
}
}
// --- Harness ---
static void check(String desc, boolean cond) {
if (cond) {
System.out.println(" PASS: " + desc);
passed++;
} else {
System.out.println(" FAIL: " + desc);
failed++;
}
}
public static void main(String[] args) {
System.out.println("=== EspIdf0002Test (CWE-312 HTTP auth password logged) ===\n");
testDefectiveLogsPassword();
testFixedDoesNotLogPassword();
testDefectiveAlsoExposesUsername();
testFixedProducesNoLogLines();
testBothReturnSameDigest();
testPasswordNotLeakedForMultipleRealms();
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
if (failed > 0) System.exit(1);
}
}

28
defects/esp-idf/SCAN.md Normal file
View file

@ -0,0 +1,28 @@
## ESP-IDF 5-MOAD Scan — 2026-03-31
Target: https://github.com/espressif/esp-idf (depth=1)
### MOAD-0001 (CWE-407) — CLEAN
WiFi core (esp_wifi) is a precompiled binary blob (no C source for station list).
Bluetooth GATT utils (gatt_utils.c) iterates fixed-size arrays bounded by
GATT_MAX_APPS (max ~10 simultaneous GATT apps) and GATT_MAX_BG_CONN_DEV — not
O(N^2) at IoT scale; array sizes are compile-time constants, not user-controlled.
NVS page scan iterates ENTRY_COUNT (126 entries per page) — bounded, not unbounded.
No O(N^2) hot-path defect found.
### MOAD-0002 (Intertangle) — CLEAN
Components are cleanly decoupled: esp_wifi, bt, nvs_flash, esp-tls, esp_http_client
each have their own init/deinit. No shared mutable god object found coupling
independent subsystems.
### MOAD-0003 (Leaked Context) — CLEAN
ESP-IDF is C-based, uses FreeRTOS tasks. No thread-local storage API misuse found.
pvTaskGetThreadLocalStoragePointer usage in components is scoped to task setup,
not request-identity leakage.
### MOAD-0004 (CWE-312) — 2 DEFECTS FOUND
See esp-idf-0001 and esp-idf-0002.
### MOAD-0005 (Thundering Herd) — CLEAN
NVS uses NVSHandleLocked with Lock RAII guard on every read/write operation.
WiFi event loop is single-threaded event dispatch. No concurrent cache stampede found.