| Unbounded allocation | A buffer pre-sized from a length/count/size field read out of the input — Vec::with_capacity(n), reserve(n), vec![0; n], Vec::with_capacity, malloc(n), new Array(n), make([]T, n) — before n is bounded against the real remaining input | A tiny input declares a huge size → multi-GB allocation → allocator abort / OOM kill |
| Unbounded decompression | Inflating attacker bytes (gzip, zlib, zstd, deflate, zip, CAB, brotli) into a buffer or temp file with no bounded-reader cap and no max-output-size check | Compression bomb: ~1 KB inflates to GBs of RAM or disk. An output-size cap closes the MEMORY bomb but NOT the CPU one: if the capped-but-large output is then parsed / scrubbed / transcoded / walked, the small-compressed→large-decompressed ratio is a CPU amplification factor — see the Algorithmic-complexity row and do not clear the area just because a size cap is present |
| Uncontrolled recursion | A function that calls itself or mutually recurses once per nesting level of the input — parsers, type/graph walkers, demanglers, XML/JSON descent, inline-tree walks — with no depth limit AND no visited-set | Deeply nested input exhausts the native stack → stack-overflow abort (uncatchable; kills the whole process, not one request) |
| Unbounded delegation to a parser/codec | Untrusted input passed to a third-party or external-crate parser, deserializer, or decompressor (XML, JSON, YAML, protobuf, msgpack, archive/compression) with no caller-side depth/size/time bound, when that library is not demonstrably bounding its own input | The unbounded recursion or allocation lives inside the dependency and is invisible at the call site, so the unguarded call is itself the defect — a crafted deeply-nested or oversized payload overflows the stack or memory two hops away from the code you are reading |
| Non-terminating loop | Following a pointer/offset/index chain from the input (next, chained, parent, link references) with no visited-set, no strictly-decreasing invariant, and no iteration cap | A self-referential or cyclic chain loops forever, pinning a core indefinitely |
| Resource leak / panic on untrusted input | .unwrap() / .expect() / unchecked index / parse().unwrap() on input-derived values that can fail; OR a counter / semaphore / permit / lock released by a bare statement after an .await or early-return instead of an RAII/defer/finally guard, so a panic-unwind or error path skips the release | A crafted input panics a worker, or permanently leaks a shared limiter slot → persistent service-wide outage |
| Super-linear output / amplification | Output or work grows faster than input: a shared/DAG node re-walked once per reference with no memoization, an interned string deep-cloned per element, a value expanded per-reference, or a dedup/intern key that includes an attacker-varied field (so caching is defeated) | A sub-KB input materializes a multi-GB output or 2^k / n× work before any size check sees it — the amplification, not the raw input, is the weapon |
| Algorithmic complexity (CPU) | A per-element step that re-scans or re-allocates the whole remaining input each iteration (O(n²) — e.g. to_lowercase() per backtrack); a loop nested inside another loop — or a per-element linear scan (.iter().find(..), get_rows(range), a re-parse) run once per item of an outer collection — where BOTH the inner and outer bounds are attacker-controlled counts (O(N·M)); a backtracking matcher / regex / glob with no step budget (ReDoS); a PEG/parser-combinator grammar rule (parsimonious, pest, nom, ANTLR, hand-rolled recursive descent) whose lookahead assertion (&expr/!expr, a peek-and-rescan) re-scans forward from every position — this hides O(N²) inside a declarative grammar rule with no visible loop or regex, so inspect grammar rule definitions themselves, not only hand-written loops; or expensive per-byte processing (scrub, transcode, decompress-then-walk) on a shared worker — this holds even when the decompressed size is capped: the cap bounds memory, but a tiny compressed payload inflating to a capped-large buffer that is then scrubbed / transcoded / parsed at a fixed per-byte cost still burns cost proportional to the cap (a ~400 KB request → a capped ~100 MiB → seconds of CPU), unbounded relative to the bytes sent | Cheap-to-send input burns seconds of CPU per request; a low request rate saturates a bounded shared worker/thread pool and stalls all tenants — no crash and no large allocation needed; resident memory can stay flat |
| ReDoS / catastrophic regex backtracking | A regex whose structure allows super-linear backtracking — nested quantifiers (a+)+, (a*)*, (.*)+; quantified alternation with overlapping/shared-prefix branches (a|a)*, (a|ab)*, (\d|\d\d)+; adjacent quantifiers over overlapping classes .*.*, \s*\s*, \d+\d+, or a repeated .*<sep>.* shape — applied via .match/.test/.exec/.replace/.split (or new RegExp(userInput)) to an attacker-controlled subject, on a backtracking engine (JS/V8, Python re, Java, PCRE, Ruby, .NET). NOT a finding on linear engines (Go regexp, Rust regex, RE2). | A short crafted string — a repeated "pump" plus one non-matching suffix — forces exponential/quadratic backtracking: one tiny request pins a core for seconds→minutes and stalls the shared event loop / worker pool. Memory stays flat; the byte-size cap does not bound match time |
| Present-but-ineffective bound | A cap / quota / limit EXISTS but (a) bounds the wrong dimension (depth not width, count not bytes, input-size not compute-cost), (b) is enforced after the cost is paid (post-materialization size check, quota after parse/convert), (c) under-counts true cost (ledger sums payload bytes, ignores per-object struct/container overhead), or (d) is dead / defaulted off (MAX = u64::MAX, off-by-default flag) | The code looks defended, so review stops — but the guard does not bound the resource the attacker actually drives, and the sink is exploitable despite a visible limit |
| Aggregate exhaustion / missing admission control | An attacker-triggerable per-request allocation or CPU cost that is individually bounded but LARGE — sized to a big per-request cap (max_attachment_size, a decompression .take(limit) output cap, a max-body buffer) — reached on a shared endpoint with no inbound-concurrency limit and no global memory/CPU budget (connection/worker permits unbounded — max_connections/the concurrency semaphore unset or None, no backpressure). Per-request review clears it because the per-request cap is real; the gap is that nothing bounds peak-per-request-cost × max-in-flight-requests | A few cheap-to-send concurrent requests (KB of traffic, NOT a volumetric flood) each force the large per-request cost at once → the aggregate exceeds the process memory/CPU budget → OOM / stall of the shared process. Report as a LEAD to verify — the admission-control config is cross-file (server/config layer, not the sink file), so flag it, do not assert a proven single-request sink |
| Per-item cap, no per-request aggregate cap | A per-item cost is individually bounded and the bound is real — a per-section/per-module/per-file size cap, a decompression .take(limit), an output-size limit applied to EACH element of a collection — but the count of such items one request/file/archive can present has no cap, and the items are retained or held concurrently (a map/list of decoded results) rather than processed-and-freed one at a time | A single request presenting N items, each individually within its cap, multiplies an individually-safe cost into an aggregate one: N × per-item-cap materialized at once. This is exploitable from ONE request with no concurrency involved at all — do not clear a per-item cap just because it is effective per item; separately check what bounds N and whether items are freed between iterations or all retained for the request's lifetime |