Skip to content

fix(score): saturate WAF request-score accumulator to prevent u16 overflow (panic / silent block-bypass) - #53

Open
seonghobae wants to merge 4 commits into
mainfrom
claude/cwlab-pr-audit-governance-1hdcp5
Open

fix(score): saturate WAF request-score accumulator to prevent u16 overflow (panic / silent block-bypass)#53
seonghobae wants to merge 4 commits into
mainfrom
claude/cwlab-pr-audit-governance-1hdcp5

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The defect

score_request (crates/waf-ids-core/src/lib.rs) accumulates the u16 request score with plain += across four sources — built-in signatures, operator threat indicators, DNSBL hits, and the anomaly heuristic. Nothing caps the number of stored threat indicators (upsert_threat / validate_threat_feed_import impose none), and each Critical match adds 100. With enough matching indicators the accumulator exceeds u16::MAX (65535), and this is the only accumulator in the crate not using saturating_add:

  • Debug / any overflow-checked build (the default cargo test dev profile) panics with attempt to add with overflow — directly violating the scorer's stated invariant that scoring never panics on arbitrary input (the whole point of a WAF).
  • Release build (overflow wraps) — the score wraps down to a small value, so the block decision (scored.score >= route.block_threshold.unwrap_or(BLOCK_SCORE)) evaluates false and a maximally-malicious request is silently not blocked. Security bypass.

Both existing guardrails deliberately dodge this rather than catch it: the libFuzzer target caps input at 32 and the proptest mirror caps threats/dnsbl at 0..16, each with a comment noting the cap avoids "trivially overflowing the u16 score accumulator." So the path was acknowledged-but-unfixed and covered by no test.

Reproduced in-container before the fix (700 matching Critical indicators):

thread '...' panicked at crates/waf-ids-core/src/lib.rs:846:13:
attempt to add with overflow

The fix

  • Switch the four accumulation sites to score.saturating_add(...) — the convention already used everywhere else in the crate (saturating_add at lib.rs:546, :906, :929, root src/lib.rs:848). The score now clamps at u16::MAX, so a saturated malicious request still scores as blockable (>= BLOCK_SCORE).
  • let mut score: u16 = 0; is now explicitly typed (the previous inference came from +=).
  • Add a deterministic core regression test score_request_saturates_instead_of_overflowing_on_many_matches (700 matching Critical indicators) — it panics on the old code and asserts score == u16::MAX and >= BLOCK_SCORE on the fix.
  • Refresh the fuzz-target comment whose cap rationale referenced the now-removed overflow (the cap stays at 32 for fuzzer-exploration efficiency; the saturation path is pinned deterministically by the new unit test).

No behavior change for non-overflowing inputs; scores below u16::MAX are unaffected.

Verification (all three CI gates)

  • cargo fmt --check → clean
  • cargo clippy --locked --workspace --all-targets -- -D warnings → clean (exit 0)
  • cargo test -p waf-ids-core9 passed (incl. the new regression test + the 3 proptest invariants)
  • cargo test --locked --workspace → the new test and all core tests pass. One unrelated pre-existing failure, load_surfaces_state_rewrite_failures, reproduces identically on the base branch in this sandbox: it sets a directory to 0o500 and expects the write to fail, but the sandbox runs as root (which bypasses permission bits), so it is a run-as-root artifact, not caused by this change, and passes in the repo's non-root CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HdCssGnNMhKHNu3TXFstWH


Generated by Claude Code

Summary by CodeRabbit

릴리스 노트

  • 버그 수정

    • 위협 평가 점수 계산의 오버플로우 방지 메커니즘 강화로 안정성 개선
  • 테스트

    • 다중 심각 지표 매칭 시나리오에 대한 회귀 테스트 추가

… overflow

`score_request` accumulated the u16 request score with plain `+=` across
built-in signatures, operator threat indicators, DNSBL hits, and the anomaly
heuristic. Threat-feed imports impose no count cap and each Critical match adds
100, so with enough matching indicators the accumulator exceeds u16::MAX:

- debug / overflow-checked builds panic ("attempt to add with overflow"),
  violating the invariant that scoring never panics on arbitrary input; and
- release builds wrap the score down to a small value, so a maximally
  malicious request scores below the block threshold and is silently NOT
  blocked (security bypass).

Switch the four accumulation sites to `saturating_add` (the convention already
used everywhere else in the crate) so the score clamps at u16::MAX and a
saturated request still scores as blockable. Add a deterministic core
regression test (700 matching Critical indicators) that panics on the old code
and asserts saturation + block on the fix, and refresh the fuzz-target comment
whose cap rationale referenced the now-removed overflow.

Verification: cargo fmt --check, cargo clippy --locked --workspace
--all-targets -- -D warnings (clean), and cargo test -p waf-ids-core (9 passed,
incl. the new test + 3 proptest invariants) all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdCssGnNMhKHNu3TXFstWH
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20f6ba4c-27d2-4985-a625-dbd7d73d2e52

📥 Commits

Reviewing files that changed from the base of the PR and between 7b72432 and 1e912b5.

📒 Files selected for processing (2)
  • crates/waf-ids-core/src/lib.rs
  • fuzz/fuzz_targets/fuzz_score_request.rs

📝 Walkthrough

Walkthrough

score_request의 점수 누적을 포화 덧셈으로 변경했다. 여러 Critical 지표가 매칭될 때 u16::MAX에 도달하는 회귀 테스트를 추가했다. 퍼즈 입력 제한 설명도 갱신했다.

Changes

점수 누적 포화 처리

Layer / File(s) Summary
점수 누적 경로 변경
crates/waf-ids-core/src/lib.rs
내장 시그니처, 구성된 위협 지표, DNSBL, 이상 탐지의 점수 누적에 saturating_add를 사용한다.
포화 점수 검증
crates/waf-ids-core/src/lib.rs, fuzz/fuzz_targets/fuzz_score_request.rs
다수의 Critical 지표가 매칭될 때 점수가 u16::MAX로 포화되고 BLOCK_SCORE 이상인지 검증한다. 퍼즈 입력 컬렉션 제한 설명에 포화 동작과 관련 코어 테스트를 명시한다.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 변경 세트의 주요 내용을 명확하게 요약합니다. u16 오버플로우 방지를 위해 포화 덧셈을 적용하는 핵심 변경사항을 정확하게 나타냅니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/cwlab-pr-audit-governance-1hdcp5

Comment @coderabbitai help to get the list of available commands.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Commercial-readiness re-trigger

Branch updated onto latest main (post-#51). CI + required OpenCode/Noema reviews re-requested so this security fix can clear require_last_push_approval and merge.

Please re-review: coverage evidence previously passed; only approval from a non-author is still missing for merge.

@seonghobae
seonghobae enabled auto-merge (squash) July 31, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants