Skip to content

Require positive confirmation for lightfuzz code-change findings - #3357

Open
liquidsec wants to merge 6 commits into
devfrom
lightfuzz-fp-improvements-6-26
Open

Require positive confirmation for lightfuzz code-change findings#3357
liquidsec wants to merge 6 commits into
devfrom
lightfuzz-fp-improvements-6-26

Conversation

@liquidsec

@liquidsec liquidsec commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Two lightfuzz submodules asserted a vulnerability from a status-code flip, without any content-level evidence that the vuln class was actually present. This came out of triaging a large batch of findings from real engagement scans, where each of these produced a recurring, systematic false-positive class.

sqli: the ' vs '' status flip proved nothing on its own

The Code Change branch emitted Possible SQL Injection (HIGH) purely from a status difference between the single-quote and doubled-quote probes. Two confounders produce that flip with no SQL involved:

  • A WAF matching its managed SQLi signature. The bare ' gets a 403, the doubled '' does not. The existing guard only suppressed this when the block page matched a WAF YARA signature, which the block pages responsible for these findings did not.
  • An envelope whose structural validity changes when the payload is repacked. Injecting into a field inside a base64/JSON envelope changes whether the envelope decodes and validates, so the status flips independently of any query.

Two gates now apply, cheapest first:

  1. is_quote_specific() appends one vs. two benign characters, mirroring '/'' in length with no SQL meaning. If the benign pair reproduces the same status triplet, the flip tracks value length or envelope validity.
  2. confirm_boolean_differential() is required to emit. A TRUE/FALSE pair must produce a content differential, with reflected payloads stripped in raw, URL-encoded and HTML-escaped form, and the TRUE payload re-sent to confirm the body is deterministic (an unstable page would otherwise fake a differential). The confirming pair is recorded in the finding description.

dev's YARA-gated 403 check is unchanged. Suppressing every transition into 403 was tried and reverted: it costs real findings on apps that answer a SQL error with 403, and the boolean gate already rejects unsigned block pages.

Time-based blind detection is untouched.

serial: URI parsing read as deserialization

Error Resolution fired when a garbage control payload returned 500 and a serialized probe returned 200. On parameters the framework parses as a URL or host, that flip is URI parsing: .NET's Uri treats everything before the first / as the hostname, so a payload whose leading token happens to be a valid host label parses, and the control payload does not. The payload's content never mattered.

Before emitting, we now send a twin of the payload with the same encoding, length and trailing bytes but a scrambled magic header. It still parses identically and deserializes under nothing. If it resolves the error too, the outcome is independent of the payload's content and the finding is suppressed.

Archived provenance on findings

Detectors running over a Wayback-archived body emitted findings with live severity and no visible indication the evidence was historical. The only signal was discovery_path.

FINDING now renders the snapshot URL via the existing but unused event.archive_url property: in output.txt as a trailing (archived: <url>), and in output.json as a top-level archive_url key. It is deliberately kept out of data, since FINDING dedups on the whole data dict and putting it there would change finding identity.

Severity is not altered. An archived finding is still worth surfacing, particularly information disclosure, where the named resources may still be live.

Notes

  • HttpCompare.parse_body() was extracted from _compare_sync so the sqli differential feeds compare_body the parsed body. Passing raw text silently bypasses the ddiff_filters that mask dynamic content.
  • Seven sqli test mocks were not boolean-injectable and would have stopped firing under the new gate. They were made genuinely vulnerable via a shared sqli_injectable_response() helper rather than relaxing assertions, which are unchanged. Those handlers also parsed the query string positionally, which breaks on payloads containing =, so they now use request.args.
  • The five new false-positive tests were verified by stashing the source fixes and re-running: all five fail against unmodified dev, so none pass vacuously.
  • A many-time-pad false-positive class (structured platform GUIDs read as reused keystream) was investigated and deliberately left in place. Every filter that suppressed it also suppressed real keystream reuse on templated plaintexts, and an unexplained value is worth surfacing even when it turns out to be benign. crypto.py is untouched.

Each of these asserted a vulnerability from a status-code flip or a
coincidental statistical signal, with no content-level evidence that the
vuln class was present.

sqli: the '/'' status flip alone now proves nothing. Suppress any
transition into 403, verify the flip is quote-specific with a benign
one-vs-two-character control, and require a TRUE/FALSE boolean pair to
produce a reproducible content differential before emitting.

serial: Error Resolution now sends a same-encoding, same-length twin of
the payload with a scrambled magic header. If that also resolves the
error, the outcome doesn't depend on the payload's content, so the value
is being parsed (e.g. as a URI host) rather than deserialized.

crypto: run the structured-identifier discrimination on every candidate
pair rather than only those with a leading-zero run, and reject pairs
whose XOR zeros out at scattered offsets, which is a shared field
template rather than two ciphertexts under one keystream.

Also surface archived provenance on findings: FINDING now renders the
snapshot URL in output.txt and as a top-level archive_url in output.json,
so an archived severity is never read as a claim about the live host.
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📊 Performance Benchmark Report

Comparing dev (baseline) vs lightfuzz-fp-improvements-6-26 (current)

📈 Detailed Results (All Benchmarks)

📋 Complete results for all benchmarks - includes both significant and insignificant changes

🧪 Test Name 📏 Base 📏 Current 📈 Change 🎯 Status
Bloom Filter Dns Mutation Tracking Performance 4.99ms 4.31ms -13.5% 🟢🟢 🚀
Bloom Filter Large Scale Dns Brute Force 17.93ms 17.45ms -2.7%
Large Closest Match Lookup 324.83ms 336.66ms +3.6%
Realistic Closest Match Workload 180.37ms 178.74ms -0.9%
Event Memory Medium Scan 1402 B/event 1402 B/event +0.0%
Event Memory Large Scan 1527 B/event 1527 B/event +0.0%
Event Validation Full Scan Startup Small Batch 402.75ms 429.87ms +6.7%
Event Validation Full Scan Startup Large Batch 511.59ms 542.39ms +6.0%
Make Event Autodetection Small 20.16ms 20.25ms +0.4%
Make Event Autodetection Large 205.47ms 206.20ms +0.4%
Make Event Explicit Types 8.49ms 8.43ms -0.7%
Excavate Single Thread Small 3.671s 3.648s -0.6%
Excavate Single Thread Large 9.182s 9.248s +0.7%
Excavate Parallel Tasks Small 3.865s 3.865s -0.0%
Excavate Parallel Tasks Large 5.866s 5.863s -0.0%
Intercept Throughput Small 872.49ms 878.83ms +0.7%
Intercept Throughput Medium 917.32ms 870.41ms -5.1%
Dns Throughput Quiet 2.496s 2.521s +1.0%
Dns Throughput Loaded 1.829s 1.729s -5.5%
Dns Throughput Inherited 2.218s 2.220s +0.1%
Is Ip Performance 2.28ms 2.27ms -0.4%
Make Ip Type Performance 208.80µs 205.96µs -1.4%
Mixed Ip Operations 2.41ms 2.39ms -0.9%
Memory Use Web Crawl 1.0 MB 1.0 MB +1.0%
Memory Use Subdomain Enum 28.3 MB 28.3 MB +0.0%
Memory Use Deep Chain 5.0 MB 5.0 MB +0.0%
Memory Use Parallel Chains 12.2 MB 11.4 MB -6.5%
Scan Throughput 100 2.581s 2.629s +1.9%
Scan Throughput 1000 17.696s 17.847s +0.9%
Typical Queue Shuffle 5.49µs 5.31µs -3.3%
Priority Queue Shuffle 26.84µs 26.50µs -1.3%

🎯 Performance Summary

+ 1 improvement 🚀
  30 unchanged ✅

🔍 Significant Changes (>10%)

  • Bloom Filter Dns Mutation Tracking Performance: 13.5% 🚀 faster

🐍 Python Version 3.11.15

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.35088% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 90%. Comparing base (3be13de) to head (3b12370).
⚠️ Report is 1 commits behind head on dev.

Files with missing lines Patch % Lines
bbot/modules/lightfuzz/submodules/sqli.py 78% 15 Missing ⚠️
bbot/modules/lightfuzz/submodules/serial.py 78% 6 Missing ⚠️
.../test_step_2/module_tests/test_module_lightfuzz.py 99% 1 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff           @@
##             dev   #3357    +/-   ##
======================================
- Coverage     90%     90%    -0%     
======================================
  Files        450     450            
  Lines      46327   46568   +241     
======================================
+ Hits       41588   41800   +212     
- Misses      4739    4768    +29     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

liquidsec added a commit that referenced this pull request Jul 30, 2026
Comment thread bbot/modules/lightfuzz/submodules/crypto.py Outdated
Comment thread bbot/modules/lightfuzz/submodules/crypto.py
Comment thread bbot/modules/lightfuzz/submodules/sqli.py Outdated
Comment thread bbot/modules/lightfuzz/submodules/sqli.py
Comment thread bbot/test/test_step_2/module_tests/test_module_lightfuzz.py Outdated

@singlerider singlerider left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the sqli work fixes real FPs. Two blockers, plus general cleanup.

Blocking: the interior-zeros rule in crypto.py takes many-time-pad detection from 500/500 to 0/500. The blanket 403 suppression is redundant, 119/119 pass without it. Details inline.

Comment density. The new detection code is roughly a third prose. crypto.py is +21 lines with ~8 comment lines, sqli.py +120 with ~14. Several restate the code, and one of them is actively wrong now: the interior-zeros comment asserts ciphertexts only zero at the leading run or shared suffix, which is the false premise driving the detection loss. Long explanatory blocks age badly, prefer naming things so they need less narration. LLMs tend to read comments s "truth." Excessive comments become an absolute drift surface.

Magic numbers. >= 2, <= 4, 0.7, 0.9, min(4, ...), 0x55, and the 403/429 literals are all unnamed detection thresholds. Precedent: PER_PARENT_CAP in chaos.py.

Hardcoded digests in tests. The two 32-hex GUIDs are load-bearing, and the docstring says they clear the 0.9 threshold "by coincidence". Nothing asserts the coincidence still holds, so a threshold change makes it pass for the wrong reason.

Clean: no tests removed (113 to 118, the 152 deleted lines are consolidation into sqli_injectable_response), corrupt_payload edge cases correct, archive_url pre-exists on dev.

@liquidsec

Copy link
Copy Markdown
Collaborator Author

Trimmed in 3b1237044: sqli.py 19 prose lines to 13, serial.py 6 to 4, diff.py 3 to 2, base.py 4 to 1. crypto.py went to zero when the rule was yanked, which took the wrong comment with it.

Kept what the code can't show (entity spellings, magic-header bytes, why an extra request exists), cut what restated it. The FP test class comments stay, since the app shape each negative test models isn't visible in a handler that just returns status codes.

@liquidsec

Copy link
Copy Markdown
Collaborator Author

@singlerider all comments have been addressed

@liquidsec
liquidsec requested a review from singlerider August 5, 2026 01:54

@singlerider singlerider left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All prior blockers cleared. Verified locally.

  • crypto interior-zeros rule and blanket 403 both yanked, crypto.py back to dev verbatim.
  • &#39; and the other three entity spellings now stripped.
  • thresholds named, comment density down.

Ran the file: 119/119. Stashed the source fixes and re-ran the five FP tests plus test_event_archived_provenance against dev: all six fail, so none pass vacuously. CI green on 3b12370.

Non-blocking: archive_url is in json() but not the round-trip. event_from_json reads from data, and the pydantic/SQL Event models drop unknown keys, so a persisted-then-reloaded finding loses the [ARCHIVED] marker. Fine for output.txt/json (both render off the live event); only matters for SQL/mongo consumers. Worth a PR-body line or a follow-up.

Approving.

return f"z{payload[1:]}" if payload[0] != "z" else f"q{payload[1:]}"
try:
data = bytes.fromhex(payload) if encoding == "hex" else base64.b64decode(payload)
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to set a better precedent of error propagation. Like, if there's an error here, it will drop silently. Per the Zen of Python:

Errors should never pass silently.
Unless explicitly silenced.

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