Skip to main content
Security Engine version:
Version: v1.8

Hooks reference

This page references available expr helpers in the dedicated bot detection hooks (on_challenge_submit and on_challenge), along with the fingerprint object.

For the generic hook phases (on_load, pre_eval, post_eval, on_match) and generic helpers, see the main Hooks page.

on_challenge_submit

This hook fires when a client POSTs a challenge response to /crowdsec-internal/challenge/submit, after the AppSec component has cryptographically validated the submission and decrypted the fingerprint, but before the success cookie is issued. This is the right place to refuse cookies to clients the challenge has positively identified as automation. In-band only.

Note that the default behavior is to accept (grant cookie) to client that submit a valid challenge response.

Available helpers

Helper NameTypeDescription
RejectSubmissionfunc(reason str, verbosity str?)Refuse to issue a challenge cookie despite a valid crypto submission. reason is recorded in logs. Optional verbosity: "minimal", "info" (default), "verbose" — controls how much fingerprint detail is logged.
GrantChallengeCookiefunc(reason str, ttl str?)Issue the challenge cookie inline as part of the submit response (no 307 redirect). reason is recorded in logs; optional ttl (a Go duration like "24h") overrides the configured cookie_ttl.
LogAcceptedfunc(msg str, verbosity str?)Emit a structured "submission accepted" log line. Same verbosity semantics as RejectSubmission.
EvaluateMismatchesfunc() MismatchReportSame as in on_challenge — run the mismatch checks against the just-decrypted fingerprint.
fingerprintobjectThe decoded fingerprint object — see The fingerprint object.
reqhttp.RequestOriginal HTTP request received by the remediation component. Needed by fingerprint helpers that compare against request headers, e.g. fingerprint.AcceptLanguageMismatch(req).
DumpFingerprintfunc(label str) strAppend the just-decrypted fingerprint (plus request context) as one JSONL line to a dump file, for offline analysis. Returns the file path. See DumpFingerprint.
CancelAlertfunc()Suppress the alert this rejection would otherwise produce. The cookie is still refused, only the alert is dropped.
SendAlertfunc()Re-enable the alert after a previous CancelAlert().
Score helpersAddRequestScore, RequestScore, RequestScoreReasons, RequestScoreDetail, RequestScoreFor. See Request scoring.

RejectSubmission() and GrantChallengeCookie() are terminal: both halt the remaining on_challenge_submit rules, so a later catch-all cannot undo the decision.

Example

YAML
on_challenge_submit:
- filter: EvaluateMismatches().Has("cdp")
apply:
- AddRequestScore(100, "cdp")
- filter: RequestScore() >= 75
apply:
- RejectSubmission("request score " + string(RequestScore()), "verbose")
- apply:
- LogAccepted("challenge submission accepted") #this is optional, LogAccepted isn't needed to grant a cookie to the client.

Request scoring

Rather than accepting or rejecting on a single signal, you can accumulate points across signals and act on the total. This is how the shipped bot-challenge collection works: one config adds the points, another decides the threshold.

The score is per request, starts at zero, and is available in pre_eval, post_eval, on_match, on_challenge and on_challenge_submit.

Helper NameTypeDescription
AddRequestScorefunc(points int, reason str)Add points to the request score under reason. Adding the same reason twice accumulates. Points may be negative.
RequestScorefunc() intThe current total.
RequestScoreReasonsfunc() []strThe reasons that contributed, in the order they were added.
RequestScoreDetailfunc() strThe breakdown as a stable string, "cdp=100,utc_timezone=15". This is what lands in the alert as request_score_reasons.
RequestScoreForfunc(reason str) intThe points recorded under one reason, 0 if it never fired.

The score and its breakdown are attached to any alert the request produces, so a rejection stays explainable after the fact. See Reading a rejection.

on_challenge

This hook fires for in-band requests that carry a valid __crowdsec_challenge cookie (clients that have already passed the JavaScript challenge). The decoded device fingerprint is available, so this is the right place to apply per-request decisions based on what the challenge learned about the client. Skipped if the request has no valid challenge cookie. In-band only.

Available helpers

Helper NameTypeDescription
SendChallengefunc()Force a re-challenge for this request even though the client already has a cookie (e.g. when fingerprint mismatches indicate the cookie may have been replayed).
SetChallengeDifficultyfunc(level str)Override the proof-of-work difficulty for the next challenge issued. See Challenge difficulty levels.
SetRemediationfunc(action str)Set the remediation returned to the bouncer for this request. The only special value is allow (don't block); any other value is passed through as-is. See SetRemediation*.
SetReturnCodefunc(code int)Set the HTTP status code returned to the bouncer for this request.
DropRequestfunc(reason str)Block this request immediately (using the config's default remediation) based on what the fingerprint revealed. reason is recorded in logs.
reqhttp.RequestOriginal HTTP request received by the remediation component. See req object.
IsInBandbooltrue if the request is in the in-band processing phase (always true here — on_challenge is in-band only).
EvaluateMismatchesfunc() MismatchReportRun the configured mismatch checks against the fingerprint and return a structured report. Result is cached per request. See The MismatchReport object.
fingerprintobjectThe decoded fingerprint object. See The fingerprint object.
fingerprint.UAMobileMismatchfunc() booltrue if the mobile signals carried by the fingerprint contradict the User-Agent header.
fingerprint.AcceptLanguageMismatchfunc(req http.Request) booltrue if the Accept-Language header is inconsistent with the languages reported by the fingerprint.
fingerprint.TimezoneCountryMismatchfunc(country str) booltrue if the timezone reported by the fingerprint is inconsistent with the given country code (typically obtained from a GeoIP lookup on the client IP).

Example

YAML
on_challenge:
- filter: EvaluateMismatches().High() >= 1
apply:
- SendChallenge()

Known bots

Two helpers, available in pre_eval, post_eval and on_match, let you keep legitimate non-browser clients out of the challenge flow.

MatchKnownBot

MatchKnownBot(ip, ua, path, ...files) returns true when the request matches a bot definition in one of the named files. You pass the bot files to consult explicitly (e.g. "legit_bots/gptbot.json"); the helper only queries those, and matches if any of them matches. Matching a User-Agent alone is never enough: the source IP must also match the vendor's published ranges or pass a forward-confirmed reverse-DNS check (FCrDNS). The helper is fail-closed — an unparseable address, a DNS failure, or an unknown file returns false, so the request falls through to the normal challenge.

The bot definitions are loaded from <datadir>/legit_bots/*.json. The hub ships and updates them via the crowdsecurity/appsec-bot-challenge-exclude-* appsec-configs (search-engines, ai-crawlers, social, monitoring), which both call MatchKnownBot and declare the files they need in their data: section; you can add your own — see Authoring your own known-bot files for the file format. The shipped exclude-configs use it in pre_eval to exempt verified bots before the challenge is served:

YAML
pre_eval:
- filter: MatchKnownBot(req.RemoteAddr, req.UserAgent(), req.URL.Path, "legit_bots/gptbot.json")
apply:
- ExemptFromChallenge("gptbot")

Once ExemptFromChallenge(reason) has flagged a request, SendChallenge() becomes a no-op for the rest of that request, so the exempted client is never challenged.

ExemptFromChallenge vs GrantChallengeCookie

Both keep a client out of the challenge, but at different scopes:

HelperScopeCookieUse for
ExemptFromChallenge(reason)The current request onlynoVerified known bots, well-known paths (robots.txt, /.well-known/*, feeds, webhooks) and per-request allowlisting where no state should persist. reason labels the exemption in logs and the cs_appsec_challenge_exempt_total metric.
GrantChallengeCookie(reason, ttl?)Persists across requests (until the cookie expires)yesTrusted user-agents or internal probes you want to let through for a whole session.

Challenge difficulty levels

SetChallengeDifficulty(level) accepts the following levels. Each level is a number of leading zero bits the client has to find, so the expected work doubles with every bit. Hash counts are the average a client has to compute, solve times are rough wall-clock measurements on a modern desktop browser and on a low-end mobile device.

LevelDifficultyApprox. hashesApprox. solve time (desktop / low-end mobile)When to use
"disabled"0 bits0 (any nonce wins)instantFunctional smoke testing or when you only care about the fingerprint, not the proof-of-work.
"low"18 bits2^18, ~262 0000.03 s / 0.5 sLatency-sensitive endpoints, mobile-heavy traffic.
"medium"20 bits2^20, ~1 050 0000.10 s / 2 sDefault. Reasonable trade-off between user experience and attacker cost.
"high"22 bits2^22, ~4 190 0000.41 s / 8 sRoutes under active abuse; clients you already suspect.
"impossible"256 bitsunsolvablen/aHard block: the AppSec component rejects the submission server-side. Use to fully block a client without leaking the reason.

DumpFingerprint

DumpFingerprint(label) is the fingerprint counterpart of DumpRequest: a threat-hunting aid that writes the decoded challenge fingerprint to disk so you can inspect it offline. It is available in the post_eval and on_challenge_submit hooks (the phases where a fingerprint is present).

Each call appends one JSON object per line (JSONL) — the fingerprint plus request context (client IP, remote address, User-Agent, host, URI, method, and a UTC timestamp) — to:

TEXT
<datadir>/fingerprint_dumps/crowdsec_fp_dump_<label>.jsonl

The label names the file (so you can separate dumps by purpose, e.g. "suspected-automation"), and the call returns the path it wrote to. No configuration is required — the directory is created automatically. The call is a no-op (it logs a warning and returns an empty string) if no fingerprint is attached to the request or the dump directory cannot be created.

YAML
on_challenge_submit:
- filter: fingerprint.IsBot()
apply:
- DumpFingerprint("fast-bot-detection")

The fingerprint object

In on_challenge and on_challenge_submit hooks, fingerprint exposes the device data collected by the in-browser library. It has three layers: helper methods for the common decisions, a Bot roll-up of the individual fast-bot signals, and the raw signal tree underneath when you need to branch on one specific measurement.

Accessing numeric and boolean leaves

Most leaves under fingerprint.Signals.* are wrapped so a malformed value from the browser can't abort the whole submission. Read them through an accessor: .Bool() for booleans, .Int() for numbers — e.g. fingerprint.Signals.Device.Memory.Int() or fingerprint.Signals.Automation.Webdriver.Bool(). String leaves are read directly. The fingerprint.Bot.* booleans are read directly; only fingerprint.Bot.DetectedCount needs .Int().

This page enumerates the fields; the always-current source of truth is the exported Go type FingerprintData.

Most of what this object exposes is also reachable as a scored reason key. If you are deciding whether to allow a client rather than inspecting one, Request scoring and the reason table are usually the better entry point.

Top-level fields

FieldAccessDescription
fingerprint.IsBotmethodSee verdict & signal helpers — the recommended entry point.
fingerprint.FSIDstringPer-fingerprint identifier, stable across the cookie's lifetime. Useful for correlating logs.
fingerprint.NoncestringOne-time nonce used in the challenge proof-of-work.
fingerprint.TimeintUnix-millisecond timestamp of when the fingerprint was collected in the browser.
fingerprint.URLstringThe URL the fingerprint was collected from.
fingerprint.FastBotDetection.Bool()The raw library verdict that IsBot() wraps.
fingerprint.Allowlistedbooltrue if the cookie was minted via GrantChallengeCookie(...) rather than a real challenge submission.
fingerprint.AllowlistReasonstringOperator-supplied reason from GrantChallengeCookie(reason, ...), copied through to logs.
fingerprint.SignalsobjectThe full collected fingerprint tree. See Raw signal tree.
fingerprint.BotobjectPer-signal booleans, rolled up from the fast-bot-detection library. See Bot signals.

Verdict and signal helpers

These methods roll the raw signals up into the decisions rules usually need — reach for these first:

HelperReturnsDescription
fingerprint.IsBot()boolThe recommended verdict: true if the in-browser fast-bot-detection library flagged the client.
fingerprint.HasBotSignal()booltrue if any fast-bot-detection signal fired.
fingerprint.BotSignalCount()intHow many distinct library signals fired.
fingerprint.BotSignals()[]strThe names of the library signals that fired, in stable order (e.g. ["cdp"]). The custom mismatch checks (ua_mobile, accept_language, timezone_country) are not included here — use EvaluateMismatches() for those.
fingerprint.HasAutomationSignal()boolA webdriver / Selenium / CDP / Playwright / bot-UA indicator was seen.
fingerprint.HasHeadlessSignal()boolHeadless-browser indicators (headless screen resolution, missing Chrome object, SwiftShader renderer, inconsistent ETSL).
fingerprint.HasMismatchSignal()boolCross-context / cross-API inconsistencies (iframe/worker webdriver, platform, WebGL, GPU, languages).
fingerprint.HasImpossibleDeviceSignal()boolDevice specs outside plausible bounds (impossible memory / high CPU count).

Convenience accessors

Shortcuts that read one field out of the signal tree and hand back a native value (no .Int()/.Bool() needed):

HelperReturnsDescription
fingerprint.UserAgent()stringThe User-Agent reported by the browser.
fingerprint.Platform()stringBrowser-reported platform, preferring the high-entropy client-hint value; falls back to navigator.platform.
fingerprint.Timezone()stringThe IANA timezone reported by the browser.
fingerprint.Language()stringThe browser's primary language.
fingerprint.IsMobile()booltrue if the browser advertises a mobile form factor (via UA client hints).
fingerprint.CPUCount()intnavigator.hardwareConcurrency.
fingerprint.Memory()intnavigator.deviceMemory, in GB.

Atomic mismatch checks

These three predicates compare the fingerprint against request or geo context. EvaluateMismatches() aggregates them (and every library signal) into one report, under the reason keys ua_mobile, accept_language and timezone_country.

Reach for EvaluateMismatches().Has("accept_language") rather than the atomic form in most rules: the report is cached per request and evaluating it bumps the per-signal Prometheus counters, which calling the predicate directly does not. The atomic checks are there for when you want the raw answer without touching the report.

HelperReturnsDescription
fingerprint.UAMobileMismatch()booltrue if the User-Agent claims a mobile form factor but the reported viewport width is implausibly wide (≥ 1000px).
fingerprint.AcceptLanguageMismatch(req)booltrue if the request's Accept-Language header disagrees with the fingerprint's navigator.language at the base-language level.
fingerprint.TimezoneCountryMismatch(country)booltrue if the fingerprint's timezone is implausible for the given ISO-3166 country code (typically from a GeoIP lookup). Soft signal — travelers and VPN users trigger it, so combine with other signals before blocking.

Bot signals

fingerprint.Bot.* exposes each individual fast-bot-detection signal as a boolean (read directly). The Has*Signal() helpers above are roll-ups over these; reach here to branch on one specific signal.

Most of these signals also have a reason key and a point value under the shipped scoring config: fingerprint.Bot.CDP is the same measurement as the cdp reason worth 100 points. See Reasons and severities for the mapping. Prefer the reason key in rules, since it is what EvaluateMismatches(), score_reasons and your alerts all speak.

Automation frameworks

FieldDescription
fingerprint.Bot.Webdrivernavigator.webdriver is present.
fingerprint.Bot.WebdriverWritablenavigator.webdriver is writable.
fingerprint.Bot.SeleniumA Selenium property was detected.
fingerprint.Bot.CDPChrome DevTools Protocol was detected.
fingerprint.Bot.PlaywrightPlaywright was detected.
fingerprint.Bot.BotUserAgentThe User-Agent matches a known-bot regex.

Headless browser

FieldDescription
fingerprint.Bot.HeadlessChromeScreenResolutionScreen resolution matches headless Chrome.
fingerprint.Bot.MissingChromeObjectThe window.chrome object is missing.
fingerprint.Bot.SwiftshaderRendererThe GPU renderer is SwiftShader (software rendering).
fingerprint.Bot.InconsistentEtslThe toString().length probe disagrees with the claimed browser family.

Cross-context / cross-API mismatches

FieldDescription
fingerprint.Bot.WebdriverIframeAn iframe context reports webdriver.
fingerprint.Bot.WebdriverWorkerA web-worker context reports webdriver.
fingerprint.Bot.MismatchWebGLInWorkerWebGL output differs between the main context and a worker.
fingerprint.Bot.MismatchPlatformIframeThe platform string differs inside an iframe.
fingerprint.Bot.MismatchPlatformWorkerThe platform string differs inside a worker.
fingerprint.Bot.PlatformMismatchThe UA-reported platform disagrees with navigator.platform.
fingerprint.Bot.GPUMismatchThe GPU vendor/renderer differs between contexts.
fingerprint.Bot.MismatchLanguagesnavigator.languages is internally inconsistent.

Impossible device specs

FieldDescription
fingerprint.Bot.ImpossibleDeviceMemoryReported device memory is outside plausible bounds.
fingerprint.Bot.HighCPUCountCPU count is implausibly high.

Other heuristics and aggregates

FieldDescription
fingerprint.Bot.UTCTimezoneTimezone is UTC (more common on VMs / headless environments).
fingerprint.Bot.AnyDetectedtrue if any of the signals above fired (backs HasBotSignal()).
fingerprint.Bot.DetectedCount.Int()How many fired (backs BotSignalCount()).

Raw signal tree

fingerprint.Signals.* is the full collected fingerprint, grouped by category. The tables below cover the fields rules commonly branch on; the deeper, rarely-used sub-trees (Browser.Features, Browser.Plugins, Browser.Extensions, Codecs, ...) are shown in full in the example below. Remember the .Int() / .Bool() accessors for numeric and boolean leaves.

fingerprint.Signals.Automation

FieldAccessMeaning
.Webdriver.Bool()navigator.webdriver present.
.WebdriverWritable.Bool()navigator.webdriver is writable.
.Selenium.Bool()Selenium property detected.
.CDP.Bool()Chrome DevTools Protocol detected.
.Playwright.Bool()Playwright detected.
.NavigatorPropertyDescriptorsstringRaw navigator property-descriptor probe.

fingerprint.Signals.Device

FieldAccessMeaning
.CPUCount.Int()navigator.hardwareConcurrency.
.Memory.Int()navigator.deviceMemory, in GB.
.Platformstringnavigator.platform.
.ScreenResolutionobjectScreen/viewport geometry (see below).
.MultimediaDevicesobjectCounts of speakers/microphones/webcams (see below).
.MediaQueriesobjectCSS media-query probes (see below).

fingerprint.Signals.Device.ScreenResolution: .Width, .Height, .PixelDepth, .ColorDepth, .AvailableWidth, .AvailableHeight, .InnerWidth, .InnerHeight (.Int()), .HasMultipleDisplays (.Bool()).

fingerprint.Signals.Device.MultimediaDevices: .Speakers, .Microphones, .Webcams (.Int()).

fingerprint.Signals.Device.MediaQueries: .PrefersColorScheme, .ColorGamut, .Pointer, .AnyPointer (string); .PrefersReducedMotion, .PrefersReducedTransparency, .Hover, .AnyHover (.Bool()); .ColorDepth (.Int()).

fingerprint.Signals.Browser (top level — see the example for the Features / Plugins / Extensions / HighEntropyValues / ToSourceError sub-trees)

FieldAccessMeaning
.UserAgentstringThe User-Agent string.
.ETSL.Int()Function.prototype.toString().length consistency probe.
.MathsstringHash of Math function outputs (engine fingerprint).
.Featuresobject~28 browser-capability booleans.
.PluginsobjectPlugin-array consistency probes.
.ExtensionsobjectDetected-extension bitmask and list.
.HighEntropyValuesobjectUA client-hint high-entropy values (platform, mobile, brands, ...).
.ToSourceErrorobjecttoSource probe (.HasToSource .Bool(), .ToSourceError string).

fingerprint.Signals.Graphics

FieldAccessMeaning
.WebGL.VendorstringWebGL unmasked vendor.
.WebGL.RendererstringWebGL unmasked renderer.
.WebGPU.VendorstringWebGPU adapter vendor.
.WebGPU.ArchitecturestringWebGPU adapter architecture.
.WebGPU.DevicestringWebGPU adapter device.
.WebGPU.DescriptionstringWebGPU adapter description.
.Canvas.HasModifiedCanvas.Bool()Canvas output looks tampered with.
.Canvas.CanvasFingerprintstringCanvas-rendering hash.

fingerprint.Signals.Locale

FieldAccessMeaning
.Internationalization.TimezonestringIANA timezone.
.Internationalization.LocaleLanguagestringIntl-reported locale language.
.Languages.Languagestringnavigator.language.
.Languages.Languages[]strnavigator.languages.

fingerprint.Signals.Contexts

FieldAccessMeaning
.Iframe.Webdriver.Bool()webdriver seen from a nested iframe context.
.Iframe.UserAgentstringUA reported inside an iframe.
.Iframe.PlatformstringPlatform reported inside an iframe.
.Iframe.Memory.Int()deviceMemory reported inside an iframe.
.Iframe.CPUCount.Int()hardwareConcurrency reported inside an iframe.
.Iframe.LanguagestringLanguage reported inside an iframe.
.WebWorker.VendorstringWebGL vendor reported inside a worker.
.WebWorker.RendererstringWebGL renderer reported inside a worker.
.WebWorker.UserAgentstringUA reported inside a worker.
.WebWorker.LanguagestringLanguage reported inside a worker.
.WebWorker.PlatformstringPlatform reported inside a worker.
.WebWorker.Memory.Int()deviceMemory reported inside a worker.
.WebWorker.CPUCount.Int()hardwareConcurrency reported inside a worker.

Full example

A complete decoded fingerprint as written by DumpFingerprint and exposed under fingerprint in expr. Every category is populated here so nothing is left to guess; the Bot roll-up and the helper methods above are all derived from this same data.

JSON
{
"signals": {
"automation": {
"webdriver": false,
"webdriverWritable": false,
"selenium": false,
"cdp": false,
"playwright": false,
"navigatorPropertyDescriptors": "ok"
},
"device": {
"cpuCount": 8,
"memory": 8,
"platform": "MacIntel",
"screenResolution": {
"width": 1920, "height": 1080, "pixelDepth": 24, "colorDepth": 24,
"availableWidth": 1920, "availableHeight": 1055,
"innerWidth": 1280, "innerHeight": 720, "hasMultipleDisplays": false
},
"multimediaDevices": { "speakers": 1, "microphones": 1, "webcams": 1 },
"mediaQueries": {
"prefersColorScheme": "light", "prefersReducedMotion": false,
"prefersReducedTransparency": false, "colorGamut": "srgb",
"pointer": "fine", "anyPointer": "fine", "hover": true, "anyHover": true,
"colorDepth": 24
}
},
"browser": {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
"features": {
"bitmask": "1f3a", "chrome": true, "brave": false, "applePaySupport": false,
"opera": false, "serial": true, "attachShadow": true, "caches": true,
"webAssembly": true, "buffer": false, "showModalDialog": false, "safari": false,
"webkitPrefixedFunction": true, "mozPrefixedFunction": false, "usb": true,
"browserCapture": false, "paymentRequestUpdateEvent": true, "pressureObserver": true,
"audioSession": false, "selectAudioOutput": true, "barcodeDetector": true,
"battery": true, "devicePosture": false, "documentPictureInPicture": true,
"eyeDropper": true, "editContext": true, "fencedFrame": false, "sanitizer": false,
"otpCredential": true
},
"plugins": {
"isValidPluginArray": true, "pluginCount": 5, "pluginNamesHash": "a1b2c3",
"pluginConsistency1": true, "pluginOverflow": false
},
"extensions": { "bitmask": "00", "extensions": [] },
"highEntropyValues": {
"architecture": "arm", "bitness": "64",
"brands": [
{ "brand": "Chromium", "version": "148" },
{ "brand": "Google Chrome", "version": "148" }
],
"mobile": false, "model": "", "platform": "macOS",
"platformVersion": "15.0.0", "uaFullVersion": "148.0.0.0"
},
"etsl": 33,
"maths": "d41d8cd9",
"toSourceError": { "toSourceError": "", "hasToSource": false }
},
"graphics": {
"webGL": { "vendor": "Google Inc. (Apple)", "renderer": "ANGLE (Apple, Apple M3, OpenGL 4.1)" },
"webgpu": { "vendor": "apple", "architecture": "metal-3", "device": "", "description": "" },
"canvas": { "hasModifiedCanvas": false, "canvasFingerprint": "9f8e7d6c" }
},
"codecs": {
"audioCanPlayTypeHash": "c1a2", "videoCanPlayTypeHash": "b3d4",
"audioMediaSourceHash": "e5f6", "videoMediaSourceHash": "a7b8",
"rtcAudioCapabilitiesHash": "c9d0", "rtcVideoCapabilitiesHash": "e1f2",
"hasMediaSource": true
},
"locale": {
"internationalization": { "timezone": "Europe/Paris", "localeLanguage": "en-US" },
"languages": { "languages": ["en-US", "en", "fr"], "language": "en-US" }
},
"contexts": {
"iframe": {
"webdriver": false, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
"platform": "MacIntel", "memory": 8, "cpuCount": 8, "language": "en-US"
},
"webWorker": {
"vendor": "Google Inc. (Apple)", "renderer": "ANGLE (Apple, Apple M3, OpenGL 4.1)",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
"language": "en-US", "platform": "MacIntel", "memory": 8, "cpuCount": 8
}
}
},
"fsid": "FS1_9f2c8a1b",
"nonce": "b7c1e2f0",
"time": 1770669806462,
"url": "https://example.com/checkout",
"fastBotDetection": false
}

For the higher-level bot detection workflow (what the library actually detects, how to allowlist legitimate bots, behavioral scenarios), see Bot detection.

The MismatchReport object

EvaluateMismatches() returns a cached-per-request MismatchReport summarising every mismatch signal that fired against the current fingerprint. It aggregates the library-native bot signals and the CrowdSec custom checks (ua_mobile, accept_language, timezone_country) into one severity-scored report.

MethodReturnsDescription
.Count()intTotal number of signals fired.
.Empty()booltrue if no signal fired.
.High() / .Medium() / .Low()intCount of fired signals by severity.
.BySeverity(sev str)intCount of fired signals at the given severity ("high", "medium", "low") — the generic form of .High() / .Medium() / .Low().
.Has(reason str)booltrue if the specific signal reason fired.
.Reasons()[]stringStable-ordered list of fired reason keys.
.String()strCompact human-readable form: "reason1(sev),reason2(sev)". Useful in logs.

Reasons and severities

The reason keys accepted by .Has(reason) and returned by .Reasons(), with their severity and the points the shipped scoring config gives them.

Severity and points are two different things. Severity is fixed in the engine and describes how confident the signal is. Points come from crowdsecurity/appsec-bot-challenge-scoring and describe how much that confidence should count against a client, which is a judgement call you can re-weight. That is why several high-severity signals are worth 30 rather than 100: they are reliable readings that odd-but-real setups still produce.

ReasonSeverityPointsMeaning
cdphigh100Chrome DevTools Protocol detected.
webdriverhigh100navigator.webdriver present.
webdriver_writablehigh100navigator.webdriver is writable.
seleniumhigh100Selenium property detected.
playwrighthigh100Playwright detected.
webdriver_iframehigh100An iframe context reports webdriver.
webdriver_workerhigh100A web-worker context reports webdriver.
bot_user_agenthigh100User-Agent matches a known-bot regex.
headless_screen_resolutionhigh50Screen resolution matches headless Chrome.
missing_chrome_objecthigh50window.chrome object missing.
impossible_memoryhigh50Reported device memory outside plausible bounds.
inconsistent_etslhigh50toString().length probe disagrees with the claimed browser family.
mismatch_webgl_workerhigh50WebGL output differs between main context and worker.
mismatch_platform_iframehigh50Platform string differs inside an iframe.
mismatch_platform_workerhigh50Platform string differs inside a worker.
platform_mismatchhigh30UA-reported platform disagrees with navigator.platform.
gpu_mismatchhigh30GPU vendor/renderer differs between contexts.
high_cpu_counthigh30CPU count implausibly high.
utc_timezonemedium15Timezone is UTC (more common on VMs / headless).
ua_mobilemedium15Mobile UA but implausibly wide viewport (see UAMobileMismatch()).
accept_languagemedium15Accept-Language header disagrees with navigator.language.
swiftshader_rendererlow5GPU renderer is SwiftShader (software rendering).
mismatch_languageslow5navigator.languages is internally inconsistent.
timezone_countrylow5Timezone implausible for the geolocated country. Soft signal.

The points column applies only when a scoring config is loaded. Severity is always available through .High(), .Medium() and .Low(), whatever else you have installed.

The reason set evolves

These reasons derive from the fpscanner signals plus a few CrowdSec-authored checks, and may change as fpscanner and browsers evolve — treat the table above as the current shape, not a stable contract. The always-current source of truth is the exported Go API: KnownReasons() returns the full set the aggregator may emit, and SeverityFor(reason) gives each key's severity.

Example:

YAML
on_challenge_submit:
- filter: EvaluateMismatches().High() >= 1 && EvaluateMismatches().Has("cdp")
apply:
- RejectSubmission("high-severity-mismatch")
CrowdSec Docs
We use cookies

This site uses cookies to help us improve your experience. You can accept or decline below.