Adapters monitor by default, and detect something without configuration - #13
Merged
Conversation
…uration
TWO PROBLEMS, BOTH FOUND BY INSTALLING THIS ON A LIVE SITE
The site takes payments and files legal documents. Wiring up @webdecoy/express
per the docs and requesting the homepage returned:
{"error":"Forbidden","message":"Access denied by Web Decoy protection"}
An ordinary python-requests user agent got the same.
1. IT BLOCKED BY DEFAULT AND COULD NOT BE TOLD NOT TO
protect() returns allowed=false whenever the server-side score clears
threatScoreThreshold, which defaults to 80. The adapter then calls onBlocked,
which returns 403 — and onBlocked never received `next`, so "record the verdict
and serve the request anyway" was not expressible at all. `dryRun` on a rule
governs that rule, not the score.
So `mode` now defaults to 'monitor' on all three adapters: the verdict is
recorded and reported, and the response is whatever the application would have
sent. Set mode: 'enforce' once the recorded hits look right.
The check sits BEFORE the rule branches, not after. A rate-limit rule returning
429 is as much of an unasked-for surprise as a 403, so monitor means "changes
nothing" rather than "changes nothing except the rules". An earlier draft put it
after them and would have shipped the bug it exists to fix.
onBlocked also gains `next`, so the observe-and-continue shape is expressible
even in enforce mode.
Nobody adopts a defence by having it break their site on install. The WordPress
plugin needed the 2.3.2 safety release for this exact shape; this is the same
mistake in the artifact we hand to developers.
2. WITH NO RULES IT DETECTED NOTHING WORTH RECORDING
The unified score weights honeypot hits at 38% and attack signatures at 24%, but
user-agent at 1% and headers at 1% — deliberately, because both are trivially
spoofed. A middleware with no rules can only contribute those last two, so it
scores ~0 regardless of what it sees. Measured against production: every `sdk`
detection ever recorded scored 0, while sdk_tripwire averaged 52.5 and
bot_scanner 45.7.
Raising the user-agent weight would be the wrong fix and actively backwards — it
would score the clients honest enough to say "python-requests" and miss every
attacker who simply does not. It would also raise threat levels, and
threat_level IN ('HIGH','CRITICAL') is the membership condition for the
cross-site actor feed, so it would push traffic onto a shared customer-facing
surface as a side effect of a presentation problem.
So an SDK constructed with no rules now gets tripwires instead of nothing. The
built-in paths are secrets and config files — /.env, /.ssh/id_rsa,
/wp-config.php — that no application serves, so a request for one is a scanner
enumerating rather than a visitor browsing, and a legitimate visitor cannot trip
one by accident. `rules: []` opts out explicitly.
Nothing in the scorer changed.
cport1
force-pushed
the
fix/adapters-monitor-by-default
branch
from
July 30, 2026 23:14
2f8c7e3 to
9c0f082
Compare
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.
Both found by installing this SDK on a live site that takes payments and files legal documents. Wiring up
@webdecoy/expressper the docs and requesting the homepage returned:{"error":"Forbidden","message":"Access denied by Web Decoy protection"}An ordinary
python-requestsuser agent got the same.1. It blocked by default and could not be told not to
protect()returnsallowed: falsewhenever the server-side score clearsthreatScoreThreshold(default 80). The adapter callsonBlocked, which 403s — andonBlockednever receivednext, so "record the verdict and serve the request anyway" was not expressible.dryRunon a rule governs that rule, not the score.modenow defaults to'monitor'on all three adapters. Setmode: 'enforce'once the recorded hits look right.The check sits before the rule branches, not after. A rate-limit rule returning 429 is as much of an unasked-for surprise as a 403, so monitor means "changes nothing" rather than "changes nothing except the rules". An earlier draft put it after them and would have shipped the exact bug it exists to fix — caught by re-reading the placement against my own comment.
onBlockedalso gainsnext, so observe-and-continue is expressible even in enforce mode.Nobody adopts a defence by having it break their site on install. The WordPress plugin needed the 2.3.2 safety release for this exact shape; this is the same mistake in the artifact we hand to developers.
2. With no rules it detected nothing worth recording
The unified score weights honeypot 38%, attack signatures 24%, but user-agent 1% and headers 1% — deliberately, because both are trivially spoofed. A middleware with no rules can only contribute the 1% signals, so it scores ~0 regardless of what it sees.
Measured against production:
sdksdk_tripwirebot_scanneredge_workerRaising the user-agent weight would be the wrong fix and actively backwards — it would score the clients honest enough to say
python-requestsand miss every attacker who simply does not. It would also raise threat levels, andthreat_level IN ('HIGH','CRITICAL')is the membership condition for the cross-site actor feed — so a presentation problem would push traffic onto a shared, customer-facing surface as a side effect.So an SDK constructed with no rules now gets tripwires instead of nothing. The built-in paths are secrets and config files —
/.env,/.ssh/id_rsa,/wp-config.php— that no application serves, so a request for one is a scanner enumerating rather than a visitor browsing, and a legitimate visitor cannot trip one by accident.rules: []opts out explicitly.Nothing in the scorer changed.
Breaking change
modedefaults to'monitor', so an existing install that relied on automatic blocking will stop blocking until it setsmode: 'enforce'. That is the intended direction — failing open is the safe way to discover this.Testing
176 tests green across the monorepo,
check:edgeclean on both edge-targeted packages. New coverage: monitor serves the request and annotates what it would have done, monitor is the default, the annotation does not leak to the browser, an inbound would-block claim is stripped, tripwires are on with no config, ordinary paths are untouched, andrules: []opts out.Verified the monitor test actually catches the bug by flipping the default back to
enforceand watching it fail.