4 follow-up patches shipped: wildfly-0002 + wildfly-0003 + log4j2-0001 + nakama-0001
Acting on the 4 borderline candidates flagged in the session-summary intel. All 4 surfaced after the unmoad scanner enhancements cleared M3/M4 noise. UNDF-1306 wildfly-0002 (HIGH) - ElytronSecurityDomainContextImpl.isValid() sets currentIdentity ThreadLocal with no paired cleanup contract. Subject populated at line 69 is the canonical handover; the ThreadLocal stash leaks to next request on the pool thread. Fix: drop the .set(identity) line. UNDF-1307 wildfly-0003 (LOW) - TransactionRollbackSetupAction.depth.set(null) should be depth.remove() to fully delete the ThreadLocal entry; current pattern leaves null binding pinning the WildFly classloader during undeploy/redeploy. Functional clear, classloader-retention only. UNDF-1308 log4j2-0001 (HIGH) - Log4jMDCAdapter.clear() only clears the log4j ThreadContext map, NOT the SLF4J pushByKey/popByKey stacks (mapOfStacks ThreadLocal). SLF4J spec mandates clear() means "clear all MDC". Per-key Deques accumulate across requests. Fix: add clear() to ThreadLocalMapOfStacks (calls tlMapOfStacks.remove()) and call from the public clear(). UNDF-1309 nakama-0001 (HIGH MOAD-0004) - social/social.go logs OAuth access tokens, ID tokens, oauth2.Token objects (incl. refresh tokens), Steam publisherKey + ticket at debug level via zap.String/zap.Any. 11 call sites. Fix: replace value-logging with shape-logging (token_len, has_token bool) — preserves debug value, redacts secret bytes. First MOAD-0004 patch this session. Companion to the 3 MOAD-0003 patches (wildfly-0001/0002/0003) extending the inverse-pipeline pattern across projects: scanner enhancement -> noise reduction -> human triage finds defects that were buried. Total session flagships: 9 (was 6) — 5 CWE-407 + 3 MOAD-0003 + 1 MOAD-0004.
This commit is contained in:
parent
d149f2aaf0
commit
2eea7d128c
11 changed files with 771 additions and 0 deletions
69
whitepaper/outreach/log4j2.md
Normal file
69
whitepaper/outreach/log4j2.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Apache log4j2 — MOAD-0003 (Leaked Context) Disclosure Brief
|
||||
|
||||
**Project:** Apache log4j2 (apache/logging-log4j2)
|
||||
**Severity:** HIGH
|
||||
**CWE:** CWE-668 (Exposure of Resource to Wrong Sphere)
|
||||
**MOAD:** [MOAD-2026-0003 A Leaked Context](https://undefect.com/moad-2026-0003/)
|
||||
**Pattern:** Log4jMDCAdapter.clear() does not clear SLF4J pushByKey/popByKey stacks
|
||||
|
||||
## Defect Map
|
||||
|
||||

|
||||
|
||||
## What it is
|
||||
|
||||
`Log4jMDCAdapter` (the SLF4J→log4j MDC bridge) maintains TWO per-thread state holders:
|
||||
|
||||
1. **`ThreadContext` map** — log4j-core's canonical MDC, the one most callers think of as "MDC"
|
||||
2. **`mapOfStacks: ThreadLocalMapOfStacks`** — the SLF4J adapter's own `ThreadLocal<Map<String, Deque<String>>>` carrying the per-key stack semantics that SLF4J added with `pushByKey/popByKey/peekByKey/clearByKey/getCopyOfDequeByKey`
|
||||
|
||||
The `clear()` method clears holder #1 only. SLF4J's `MDC.clear()` spec mandates "clear all MDC state for this thread." With log4j-slf4j2-impl, the per-key Deques accumulate across requests on a re-used pool thread.
|
||||
|
||||
| Defect | UNDF |
|
||||
|--------|------|
|
||||
| `log4j2-0001` | [undf-2026-000001308](../undf-2026-000001308/) |
|
||||
|
||||
## Where it lives
|
||||
|
||||
`log4j-slf4j2-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java:55-57`:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void clear() {
|
||||
ThreadContext.clearMap();
|
||||
// mapOfStacks is NOT cleared — silently leaks pushByKey-set state
|
||||
}
|
||||
```
|
||||
|
||||
`ThreadLocalMapOfStacks` (lines 118-150) has `clearByKey()` per-key but no clear-all.
|
||||
|
||||
## Fix
|
||||
|
||||
Two-line change:
|
||||
|
||||
1. Add `clear()` to `ThreadLocalMapOfStacks` that calls `tlMapOfStacks.remove()` (also addresses minor classloader-retention in app-server thread pools).
|
||||
2. Call `mapOfStacks.clear()` from the public `Log4jMDCAdapter.clear()`.
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void clear() {
|
||||
ThreadContext.clearMap();
|
||||
mapOfStacks.clear(); // <-- new
|
||||
}
|
||||
|
||||
private static class ThreadLocalMapOfStacks {
|
||||
public void clear() {
|
||||
tlMapOfStacks.remove();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Why it matters
|
||||
|
||||
Web frameworks (Spring, Quarkus, Netty-based stacks) call `MDC.clear()` between requests. If application code uses `pushByKey/popByKey` to track per-request state (tenant IDs, trace contexts, user roles, auth tokens), Request B sees Request A's leftover stack contents on a re-used pool thread. Bounded blast radius (only stack-API callers; the most common `MDC.put/get` users are unaffected because that path goes through `ThreadContext` which IS cleared) but security-grade for callers that DO use the stack API.
|
||||
|
||||
## How it surfaced
|
||||
|
||||
Surfaced after unmoad scanner enhancement `1f48798` (Java ThreadLocal-scoped `.set()` leak detection) cleared 79% of log4j2's M3 false-positive noise (456 → 95). Manual triage of the residual 95 identified this as the only flagship-grade defect; the other 94 are layout/StringBuilder buffer ThreadLocals that are intentional performance caches with no value-leak risk.
|
||||
|
||||
This is the first patch from a project that joined the clean-scan honor roll in Wave 23 — the inverse-pipeline pattern (scanner improves SNR → triage finds defect that previously was invisible) holds beyond WildFly.
|
||||
92
whitepaper/outreach/nakama.md
Normal file
92
whitepaper/outreach/nakama.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Nakama — MOAD-0004 (Logged Secret) Disclosure Brief
|
||||
|
||||
**Project:** Nakama (heroiclabs/nakama)
|
||||
**Severity:** HIGH
|
||||
**CWE:** CWE-532 (Insertion of Sensitive Information into Log File)
|
||||
**MOAD:** [MOAD-2026-0004 A Logged Secret](https://undefect.com/moad-2026-0004/)
|
||||
**Pattern:** OAuth tokens, Steam publisher key, Game Center signatures logged at debug level
|
||||
|
||||
## Defect Map
|
||||
|
||||

|
||||
|
||||
## What it is
|
||||
|
||||
Nakama's social-auth client at `social/social.go` has 11 debug-level `zap.Field` call sites that log third-party authentication SECRETS as full string/object values. Game-server operators run nakama with debug logging enabled in development and frequently leave it on in production — log files routed to centralized aggregators (ELK, Datadog, Loki, Sumo) inherit the leaked tokens and become a credential exfiltration target.
|
||||
|
||||
| Line | Provider | Logged secret | Impact if leaked |
|
||||
|------|----------|---------------|------------------|
|
||||
| 235 | Facebook | `accessToken` | Full Graph API access for the user |
|
||||
| 250 | Facebook | `accessToken` (friends scope) | Same |
|
||||
| 290 | Facebook Instant Game | `signedPlayerInfo` | HMAC-signed authenticator; replay attack |
|
||||
| 353 | Google | `idToken` | User identity assertion; reused against ID-token APIs |
|
||||
| 435 | Google | `auth_token` (= idToken) | Same |
|
||||
| 439, 443, 448, 452 | Google | `oauth2.Token` object `t` | Contains `AccessToken` AND `RefreshToken`; refresh token grants long-lived backend access |
|
||||
| 630 | Steam | `publisherKey` + `ticket` | Server's Steam web API key; full developer Steam app access |
|
||||
|
||||
| Defect | UNDF |
|
||||
|--------|------|
|
||||
| `nakama-0001` | [undf-2026-000001309](../undf-2026-000001309/) |
|
||||
|
||||
## Where it lives
|
||||
|
||||
`social/social.go:235`:
|
||||
|
||||
```go
|
||||
c.logger.Debug("Getting Facebook profile", zap.String("token", accessToken))
|
||||
```
|
||||
|
||||
`social/social.go:439`:
|
||||
|
||||
```go
|
||||
c.logger.Debug("Exchanged an authorization code for an access token.",
|
||||
zap.Any("token", t), zap.Error(err)) // t carries AccessToken AND RefreshToken
|
||||
```
|
||||
|
||||
`social/social.go:630`:
|
||||
|
||||
```go
|
||||
c.logger.Debug("Getting Steam profile",
|
||||
zap.String("publisherKey", publisherKey), // SERVER's Steam API key
|
||||
zap.Int("appID", appID),
|
||||
zap.String("ticket", ticket))
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
Replace value logging with shape logging — log the FACT that we had a token (and its length) without logging the bytes. Standard CWE-532 remediation:
|
||||
|
||||
```go
|
||||
// Before
|
||||
zap.String("token", accessToken)
|
||||
// After
|
||||
zap.Int("token_len", len(accessToken))
|
||||
```
|
||||
|
||||
```go
|
||||
// Before
|
||||
zap.Any("token", t) // t is *oauth2.Token
|
||||
// After
|
||||
zap.Bool("has_token", t != nil)
|
||||
```
|
||||
|
||||
For Steam `publisherKey` + `ticket`, redact entirely; neither presence nor length is a useful debug signal here, both are secrets that should never appear in log records:
|
||||
|
||||
```go
|
||||
// Before
|
||||
c.logger.Debug("Getting Steam profile",
|
||||
zap.String("publisherKey", publisherKey),
|
||||
zap.Int("appID", appID),
|
||||
zap.String("ticket", ticket))
|
||||
// After
|
||||
c.logger.Debug("Getting Steam profile",
|
||||
zap.Int("appID", appID),
|
||||
zap.Int("publisherKey_len", len(publisherKey)),
|
||||
zap.Int("ticket_len", len(ticket)))
|
||||
```
|
||||
|
||||
## Discovery context
|
||||
|
||||
Documented in Wave 22 survey (`/wave22-eda-games-hpc-codecs-httpd-survey/`) as the only real MOAD-0004 finding in that wave. nakama was **excluded from the Wave 22 clean-scan honor roll** because of these real M4 findings — joining the roll requires zero real defects, not just zero false positives. Honor roll status: pending fix-acceptance upstream.
|
||||
|
||||
This is the first MOAD-0004 (Logged Secret) patch shipped this autonomous-loop session.
|
||||
Loading…
Add table
Add a link
Reference in a new issue