Conversation
Move the bypass detection into a NowafplsHelper (helpers.nowafpls) that memoizes verdicts per host via asyncio.Task so concurrent consumers share a single probe. Detection is provider-agnostic: baseline the endpoint via helpers.http_compare with a benign POST body, then compare unpadded and padded malicious bodies against that baseline. Verdict is one of no_interference / bypassed / blocked / error. Provider name for the finding description is pulled from event.host_metadata (cloudcheck's cloud_providers with type waf or cdn) when available; otherwise the finding falls back to "WAF/inspection layer". Module filter drops from provider-name whitelist to just "waf" in event.tags.
Helper gains pad_form_body / pad_json for transparent WAF-bypass padding. Lightfuzz avoid_wafs becomes tri-state (always/never/try_bypasses, default try_bypasses); prepare_request pads POSTPARAM/BODYJSON when the WAF is bypassable. Ajaxpro and generic_ssrf's POST submodule opportunistically pad their adversarial bodies. Findings emitted through a padded probe are tagged used-nowafpls.
Drops disable_post, adds try_get_as_post so GET params get retested as POST, sets avoid_wafs=try_bypasses explicitly. A padded-POST XSS is triggerable from an attacker-controlled form, so the bypass is worth attempting here.
Two independent bugs made cloud-fronted targets never detect: - per_host_only clobbered the real https URL when the http variant (a 301 redirect) arrived first. Add a status-3xx reject in filter_event so the redirect stops being probed and the real endpoint keeps its slot. - The historical 128 KiB default no longer beats Cloudflare's inspection window. Bisected against a live CF site: XSS bypasses at body offset >= 1048603 (~ 1 MiB). Bump default to 1048576.
Base module's _is_http_wildcard_host was a host-level check: when the site root looked like a catchall, every event on that host was rejected. Real endpoints on wildcarding SPAs (e.g. a 200-catchall CF site with a distinct /params_test2.php) got dropped by lightfuzz / webbrute / etc. Wrapper now probes the specific URL against the wildcard baseline that the underlying helper already returned. For WEB_PARAMETER GETPARAMs the target URL is reconstructed with the parameter baked in (bare URLs on catchall hosts collapse to the baseline, only the parameterized form diverges). Also thread the padding size into the nowafpls FINDING description so operators can tell what size worked without cross-referencing the run config. Tests cover: per-URL divergence, GETPARAM reconstruction, POSTPARAM falling back to bare URL, HttpCompareError -> None, scalar-True mocks still short-circuit, and nowafpls skipping 3xx URLs.
📊 Performance Benchmark Report
📈 Detailed Results (All Benchmarks)
🎯 Performance Summary+ 1 improvement 🚀
! 2 regressions ⚠️
28 unchanged ✅🔍 Significant Changes (>10%)
🐍 Python Version 3.11.16 |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #3336 +/- ##
======================================
+ Coverage 90% 90% +1%
======================================
Files 454 457 +3
Lines 47081 47667 +586
======================================
+ Hits 42320 42879 +559
- Misses 4761 4788 +27 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
singlerider
left a comment
There was a problem hiding this comment.
Tests pass (nowafpls/ajaxpro/generic_ssrf, test_web, and the full lightfuzz suite). The prepare_request/compare_baseline sync-to-async conversion touches every submodule; verified every call site is awaited, no un-awaited coroutines.
Two things to flag before merge:
-
Breaking config change: avoid_wafs went from bool to Literal[always,never,try_bypasses]. An existing avoid_wafs: true/false in a user config is now rejected at load time. In-repo presets are migrated but external configs will break. Recommend a coercing validator (True->always, False->never) or a breaking-change changelog note.
-
_is_http_wildcard_host in base.py was rewritten to probe the specific URL (baking in GETPARAM values) against the wildcard baseline instead of returning True. It is well tested via the new test_web cases, but it is a shared-framework behavior change with no mention in the PR body. Please call it out.
Nit: is_bypassable memoizes per-host but ignores padding_size/payload on cache hits, so if the standalone module and lightfuzz probe the same host, the first caller's params win.
# Conflicts: # bbot/test/test_step_2/module_tests/test_module_ajaxpro.py # bbot/test/test_step_2/module_tests/test_module_generic_ssrf.py
|
Both addressed.
Called out the |
lightfuzz's try_bypasses branch gated on `not result.bypassed`, which collapsed no_interference, blocked and error into a single rejection. A host that carries the `waf` tag but isn't gating payloads got zero fuzzing. Branch on the explicit status instead. The same branch applied `_post_capable` before consulting nowafpls, dropping every URL and GETPARAM event on a tagged host regardless of what the probe would have said. Padding is body-only, so that gate is only meaningful once the verdict is `bypassed`. HttpCompare.compare() reported a failed request as a baseline match. nowafpls read that as "no interference" on a killed unpadded request and as "bypassed" on a killed padded one, and wildcard detection read it as "this host is a catchall". Add `none_is_match` (default True, preserving existing behavior) and pass False from those three call sites; the wildcard paths now treat a dead probe as unknown. The reasons value on that path is also a list now rather than a bare string, which consumers were rendering as "4,0,3". Log probe verdicts through a shared BypassResult.summary that carries the error and diff reasons, and report at INFO so the result is visible without debug logging.
|
Three defects were causing WAF-tagged hosts to be skipped entirely, one of which 1.
|
|
@singlerider looks like this may have inadvertently also fixed a bug that was in stable currently. So its a high priority now. @shart123456 |
singlerider
left a comment
There was a problem hiding this comment.
Re-reviewed at cb8db4f. Both flagged items resolved, and 0d98eb6 is a real fix on top. Suites pass on the branch: lightfuzz 125, nowafpls + validate_preset 36, test_web + bypass403 + paramminer_headers 29.
🟢 avoid_wafs backward compatibility
RESOLVED ✅. Your point about the validator not rewriting the runtime config is the part I would have missed: a before-validator alone would have let true load and then behave as never, since lightfuzz.setup() reads the raw dict. Coercing in both places is correct, and test_lightfuzz_avoid_wafs_bool_coerced_at_runtime pins the half that actually decides behavior. Confirmed "bogus" and 5 still fail at load with the expected-one-of message.
🟢 _is_http_wildcard_host documented
RESOLVED ✅. The PR body now has its own section calling it out as shared framework behavior with the GETPARAM rationale and the covering test named.
🟢 none_is_match on a dead probe
Not something I caught on the first pass and it is the strongest commit in the branch. compare() returning (True, "403", ...) meant a killed request read as a baseline match, so nowafpls scored a dropped unpadded request as no-interference and a dropped padded one as a bypass, which is exactly backwards for the thing this PR exists to detect. Defaulting to True keeps every other call site unchanged, and the three that care opt out explicitly. Turning reasons into a list also fixes the "4,0,3" render in paramminer's ",".join(reasons).
🟢 _post_capable ordering
Applying it before the probe meant a WAF-tagged host dropped every URL and GETPARAM event regardless of the verdict. Gating it on STATUS_BYPASSED is right, since the padding is what needs a body in the first place.
🔴 Blocking, STATUS_ERROR now silently skips the event
filter_event lumps STATUS_ERROR in with STATUS_BLOCKED and returns a bare False, so a probe that failed for an infrastructure reason (a baseline that could not be established, a transient timeout) is indistinguishable from a WAF that held. The old code at least had one meaning. Either return the string form so the skip is reported, or treat an error as unknown and fall through to fuzzing, but a bare False on an error path loses the event with no record outside debug logging.
STATUS_BLOCKED and STATUS_ERROR each return their own filter reason instead of a bare False, and the probe verdict is logged once per host at verbose so it lands outside debug logging regardless of which module asked.
|
Addressed in 58bb592.
|
Summary
Adds a WAF-bypass technique based on prepending a large junk padding to POST bodies —
inspired by the nowafpls burp extension.
Cloudflare (and several other inline WAFs) has a bounded inspection buffer; once
the request body grows past it, the malicious payload lands after the inspection
window and reaches the app unfiltered. This branch delivers:
bbot/core/helpers/nowafpls.py— shared helper.is_bypassable(event)probesthe host once (baseline vs unpadded-malicious vs padded-malicious via
http_compare), memoized per host.pad_form_body(event, body)andpad_json(event, data)are drop-ins for adversarial POST bodies.bbot/modules/nowafpls.py— dedicated detector that emits aFINDINGwhen aWAF-tagged host is bypassable, with the padding size in the description.
avoid_wafsconfig (always/never/try_bypasses, defaulttry_bypasses). Booleans are still accepted (true->always,false->never), so configs written against the previous booleanoption keep working. Submodule
prepare_requesttransparentlypads POSTPARAM / BODYJSON probes when the host's WAF is bypassable, and flips
used_nowafpls=Trueso lightfuzz tags the findingused-nowafpls.fuzz traffic uses the same bypass on WAF-tagged hosts.
try_get_as_postso GET params get re-fuzzedas padded POSTs, catching XSS reachable from an attacker-controlled form when the
WAF blocks the raw payload.
Shared-framework change:
_is_http_wildcard_hostBaseModule._is_http_wildcard_hostno longer returnsTrueas soon as the host isflagged a wildcard responder. It now probes the specific URL against the wildcard
baseline and reports a wildcard only when the response matches it, so a URL that
diverges from the baseline is treated as a real endpoint and is no longer skipped.
For
WEB_PARAMETERGETPARAMs the probe bakes the parameter into the URL, since a barepath can be a catchall while
?name=valueis a real endpoint.This is shared framework behavior and affects every module that relies on wildcard-host
filtering, not just lightfuzz. Covered by
test_base_module_is_http_wildcard_host_per_urlinbbot/test/test_step_1/test_web.py.