moad-sweep: 9 initial findings — MOAD-0005 (Hungry Regex) x1, MOAD-0006 (Thundering Herd) x8

MOAD-0005 candidates (ReDoS):
- bleach-MOADX-0001: sanitize_css O(2^N) — 71-char input causes 12s hang HIGH

MOAD-0006 candidates (Thundering Herd / cache stampede):
- hibernate-orm-MOADX-0001: QueryInterpretationCacheStandardImpl HQL plan cache HIGH
- elasticsearch-MOADX-0001: EnrichCache "intentionally non-locking" enrich search HIGH
- hadoop-MOADX-0001: FederationJCache/FederationCaffeineCache YARN default config HIGH
- celery-MOADX-0001: BaseBackend.get_task_meta chord fan-in Redis stampede HIGH
- traefik-MOADX-0001: CNAMEFlatten data race + N*30s DNS stampede HIGH
- kafka-MOADX-0001: CachedConnectors.lookup classloader scan MEDIUM
- spring-MOADX-0001: AbstractFallbackCacheOperationSource @Cacheable cold start MEDIUM
- django-MOADX-0001: cached.Loader.get_template template compile MEDIUM

CLEAN: Consul (singleflight), Kubernetes (RWMutex), Prometheus (per-scraper),
Pulsar (AsyncLoadingCache), Cassandra (LoadingCache), Flink (CAS loop),
ZooKeeper (synchronized), Airflow (hand-rolled singleflight)
This commit is contained in:
russell@unturf.com 2026-03-29 21:09:07 -04:00
parent 1f48d1b89c
commit 9325c98470
9 changed files with 1111 additions and 0 deletions

View file

@ -0,0 +1,105 @@
# UNDF: (pending)
# bleach-MOADX-0001: BleachSanitizerFilter.sanitize_css — ReDoS O(2^N) on style attribute
## MOAD-0005 Candidate — The Hungry Regex
| Field | Value |
|-------|-------|
| ID | bleach-MOADX-0001 |
| Severity | HIGH |
| Ecosystem | bleach (Python HTML sanitizer) |
| File | `bleach/sanitizer.py` |
| Lines | 553–558 |
| Pattern | `^([-/:,#%.'"\sa-zA-Z0-9!]\|\w-\w\|'[\s\w]+'\s*\|"[\s\w]+"\|\([\d,%\.\s]+\))*$` |
| Trigger | `style="a-a-a-a-...-a-@"` — 71 chars causes ~12-second hang |
| Input vector | HTML `style` attribute value from user-submitted content |
| Input length limit | none |
## Defect
`bleach/sanitizer.py`, function `sanitize_css`, lines 553–558:
```python
parts = style.split(';')
gauntlet = re.compile(
r"""^([-/:,#%.'"\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'\s*|"[\s\w]+"|\([\d,%\.\s]+\))*$"""
)
for part in parts:
if not gauntlet.match(part):
return ''
```
The pattern `^(A|B|C|D|E)*$` where:
- `A` = `[-/:,#%.'"\sa-zA-Z0-9!]` — single char (broad set including `-`)
- `B` = `\w-\w` — three chars: word-hyphen-word
The alternation `A|B` creates exponential ambiguity: a string like `a-a-a-a-` can
be parsed as `[a][-][a][-]` (all via `A`) or `[a-a][-][a-]` (via `B` then `A`) or
countless other combinations. For each prefix position, the regex engine must
explore both interpretations.
When the input does NOT match (the trailing `$` fails), Python's NFA backtracks
through all O(2^N) parse trees, where N is the number of `a-` repetitions.
**Measurement (Python 3.x, bleach 3.0.0):**
| Input length | Time |
|---|---|
| 41 chars (`a-`×20 + `@`) | 0.011s |
| 51 chars (`a-`×25 + `@`) | 0.11s |
| 61 chars (`a-`×30 + `@`) | 0.97s |
| 71 chars (`a-`×35 + `@`) | 12.8s |
Growth factor: ~10× per 10 chars = O(2^N).
**Attack payload:**
```
style="a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-@"
```
71-char style attribute causes a ~12-second Python thread hang.
Triggering characters for the non-matching terminal: `@`, `$`, `^`, `~`, `` ` ``,
`|`, `\`, `{`, `}`, `<`, `>` — all realistic HTML/CSS chars.
**Input path:** HTML user content → `bleach.clean()` → `BleachSanitizerFilter.sanitize_token()` →
`sanitize_css()` → `gauntlet.match(part)`.
## Fix
The ambiguity is between `A` matching `-` as a single character and `B` matching
`\w-\w` as a three-character sequence. Eliminating the single-char `-` from group
`A` and keeping it only in `B` removes the ambiguity, OR use atomic grouping
(Python 3.11+ via `re` module possessive `(?>...)`), OR replace with a proper
CSS tokenizer.
**Minimal fix — remove `-` from the single-char group:**
```python
gauntlet = re.compile(
r"""^([-/:,#%.'"\sa-zA-Z0-9!]|(?<!\w)-(?!\w)|\w-\w|'[\s\w]+'\s*|"[\s\w]+"|\([\d,%\.\s]+\))*$"""
)
```
**Better fix — switch to a proper CSS tokenizer** (e.g., `tinycss2` or `cssutils`)
instead of regex-based validation. Bleach already depends on `html5lib`; a small
CSS tokenizer dependency is acceptable.
**Best fix — impose input length limit before matching:**
```python
MAX_STYLE_LEN = 1000
for part in parts[:100]: # limit number of semicolon-separated parts
part = part[:MAX_STYLE_LEN]
if not gauntlet.match(part):
return ''
```
The second pattern on line 561 (`^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$`) does NOT
exhibit catastrophic backtracking — its character class `[^:;]*` is bounded by
the `:` and `;` delimiters.
## References
- bleach 3.0.0: `bleach/sanitizer.py` lines 540–575
- Pattern first introduced: commit history shows this predates 3.0.0
- Note: bleach has a history of ReDoS issues (CVE-2020-6802 linkifier, CVE-2021-23980 cleaner)
but the CSS gauntlet pattern has not been separately reported.