{"record":{"id":"829788cd3f4dd0e7","repo":"crowdsecurity/crowdsec","slug":"sendchallenge-can-only-be-called-from-an-in-band-h","errorCode":null,"errorMessage":"SendChallenge can only be called from an in-band hook (on_challenge or post_eval)","messagePattern":"SendChallenge can only be called from an in-band hook \\(on_challenge or post_eval\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/appsec/appsec.go","lineNumber":1634,"sourceCode":"\n\tfor _, sig := range report.Signals {\n\t\tmetrics.AppsecFingerprintMismatch.With(prometheus.Labels{\n\t\t\t\"reason\":        sig.Reason,\n\t\t\t\"severity\":      sig.Severity,\n\t\t\t\"appsec_engine\": request.AppsecEngine,\n\t\t}).Inc()\n\t}\n}\n\nfunc (w *AppsecRuntimeConfig) SendChallenge(ctx context.Context, state *AppsecRequestState, request *ParsedRequest) error {\n\tif w.ChallengeRuntime == nil {\n\t\treturn errors.New(\"challenge runtime not initialized\")\n\t}\n\n\t// SendChallenge can only be called from inband.post_eval and inband.on_challenge.\n\t// as it's the same expr-env, we need to detect here.\n\tif state.CurrentPhase != PhaseInBand {\n\t\treturn errors.New(\"SendChallenge can only be called from an in-band hook (on_challenge or post_eval)\")\n\t}\n\n\t// GrantChallengeCookie earlier in the same request already minted an\n\t// allowlist cookie; refuse to overwrite it with a challenge page.\n\tif state.ChallengeBypassed {\n\t\tw.Logger.Debugf(\"SendChallenge no-op: allowlist cookie already granted this request\")\n\t\treturn nil\n\t}\n\n\t// A hook flagged this request as exempt (verified bot, well-known path, ...).\n\tif state.ChallengeExempt {\n\t\tw.Logger.Debugf(\"SendChallenge no-op: request exempt from challenge\")\n\t\treturn nil\n\t}\n\n\ttarget := w.ChallengeRuntime.Difficulty()\n\tif state.ChallengeDifficulty != nil {\n\t\ttarget = *state.ChallengeDifficulty","sourceCodeStart":1616,"sourceCodeEnd":1652,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/appsec/appsec.go#L1616-L1652","documentation":"SendChallenge drives the challenge/captcha flow for a request, but that flow only exists in the in-band phase of request processing (during on_challenge or post_eval hooks, which share the same expression environment). Calling it when the request state's CurrentPhase is not PhaseInBand is refused, because out-of-band processing cannot mint challenge pages or cookies.","triggerScenarios":"Invoking the SendChallenge expression function from an out-of-band hook (e.g. outofband.on_load or outofband pre/post-eval), or from any phase other than inband.on_challenge / inband.post_eval.","commonSituations":"A user copies a SendChallenge call into an out-of-band rule collection; a shared rule set is loaded both in-band and out-of-band and the hook runs in the out-of-band path.","solutions":["Move the SendChallenge call into an in-band hook: inband.on_challenge or inband.post_eval","If the intent is to block bot traffic in out-of-band processing, use a ban/drop remediation instead of SendChallenge","Ensure the rule collection containing the call is only loaded under the in-band WAF component"],"exampleFix":"// before (out-of-band rule)\noutofband:\n  post_eval:\n    - SendChallenge()\n\n// after\ninband:\n  post_eval:\n    - SendChallenge()","handlingStrategy":"validation","validationCode":"if state.CurrentPhase != PhaseInBand {\n    return fmt.Errorf(\"SendChallenge requires in-band phase; current phase: %s\", state.CurrentPhase)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only reference SendChallenge in inband.on_challenge or inband.post_eval hooks","Do not share rule sets containing SendChallenge between in-band and out-of-band components"],"tags":["appsec","hook","state-machine"],"backgroundTag":"invalid-state-transition","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}