Repository navigation
fix(gates): block pages hidden behind inline images returned as successes - #3
Merged
Merged
Conversation
- classify_botwall scanned only the first 600 chars; a block page that embeds base64 logos (Indeed: ~7.2k chars) pushed 'You have been blocked' out of that window, so it passed as a tier_4 OK success - the same image bytes cleared the 2000-char length floor on their own - gates now strip inline data: payloads before scanning and measuring; the returned markdown is unchanged - add Cloudflare's WAF block copy 'you have been blocked' as a marker - regression tests mirror the real Indeed page shape
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
A bot-wall page that embeds its logo as an inline base64 image was being returned as a successful scrape. Callers got the text "Request Blocked. You have been blocked…" labelled
source_method: tier_4, and botwall recorded tier_4 as the host's winning tier. This PR makes the quality gates ignore inlinedata:image bytes when they scan and measure a page, and it adds Cloudflare's WAF block wording as a recognised wall.switchback/policy/gates.py(+21/−4), 5 regression tests, CHANGELOG entry under Unreleased.The bug
Scenario (real, reproduced 2026-09-27)
Before (on
main): the cascade reports success at tier_4.The "content" it returned (base64 elided):
Root cause
There are two checks in
policy/gates.py, and the two inline logos (7,184 of the page's 7,719 chars, 93%) beat both of them:classify_botwallchecklength floorA third, smaller gap: "you have been blocked" (Cloudflare's WAF / error 1020 block copy) wasn't in
_BOTWALL_MARKERS, so it wouldn't have matched even inside the window.Why it matters
scrape()returns a block page as if it were the article. Nothing in the result says it failed.tier_4as the winner for the host, so later runs start at a tier that doesn't actually work there.The fix
classify_botwallscans the head after stripping inline data URIs. The browser tiers' mid-renderlooks_blocked()calls through here, so they're fixed too.checkmeasures length, and runs the nav-shell check, on the stripped text. It still returns the originalmd, so callers who want the images keep them.("you have been blocked", "cloudflare"), placed with the vendor-specific phrases ahead of the generic ones.Every tier reaches these gates, either through
check()(tiers 1–7) or_browser.looks_blocked()(tiers 4–5), so this one file covers the whole cascade.Accepted output (what "correct" looks like)
After (this branch): the same URL is correctly classified as a wall, and the cascade falls through.
OK, md_len=7719botwall, vendorcloudflarescrape()resultscrape_detailed()ok=True, final_outcome="ok"ok=False, final_outcome="all_failed", error_class="botwall"tier_4Failing on this IP is the correct result. The page really is blocked, and an honest failure is better than a fake success. With
FIRECRAWL_API_KEYorBU_CDP_URLset, the cascade now reaches those tiers instead of stopping early.No regressions on real content
Re-ran known-good pages through the full cascade on this branch:
mainTests
tests/test_quality_gate.py: 5 new tests, offline, pure unit. The fixture mirrors the real Indeed page (same logo sizes, placeholder IP/Ray ID).test_block_page_behind_inline_images_is_botwallBotWall(vendor="cloudflare")test_cloudflare_waf_block_page_is_botwalltest_inline_images_do_not_count_toward_length_floorShortContenttest_real_article_with_inline_images_passes_unchangedtest_block_phrase_deep_in_article_body_passesmainthe three bug tests fail withDID NOT RAISE, and the two guard tests pass.pytest -q→ 36 passed (31 existing + 5 new).pytest -q tests/test_quality_gate.py # 10 passedReview notes / things to decide
short_content, where before it passed. That's the point of the fix, but it's the one place real output could change. I found no such case in the regression URLs.short_content(~540 chars < 2000) instead ofbotwall. The marker makes the vendor learnable (botwall/cloudflare), which drives egress escalation.md_lenin the orchestrator'sOKline still counts raw length, including images. I left it alone because it describes the payload returned, not what the gate measured.is_cf_challenge(solvable-challenge detection). A WAF block isn't solvable by tier_3, so it correctly stays out of_CF_CHALLENGE_MARKERS.