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:
russell@unturf.com 2026-04-26 12:30:28 -04:00
parent d149f2aaf0
commit 2eea7d128c
No known key found for this signature in database
11 changed files with 771 additions and 0 deletions

View 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
![]({static}/uploads/intel-log4j2.svg)
## 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.

View 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
![]({static}/uploads/intel-nakama.svg)
## 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.