crowdsecurity/crowdsec · error

SendChallenge can only be called from an in-band hook (on_ch

Error message

SendChallenge can only be called from an in-band hook (on_challenge or post_eval)

What it means

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.

Source

Thrown at pkg/appsec/appsec.go:1634

	for _, sig := range report.Signals {
		metrics.AppsecFingerprintMismatch.With(prometheus.Labels{
			"reason":        sig.Reason,
			"severity":      sig.Severity,
			"appsec_engine": request.AppsecEngine,
		}).Inc()
	}
}

func (w *AppsecRuntimeConfig) SendChallenge(ctx context.Context, state *AppsecRequestState, request *ParsedRequest) error {
	if w.ChallengeRuntime == nil {
		return errors.New("challenge runtime not initialized")
	}

	// SendChallenge can only be called from inband.post_eval and inband.on_challenge.
	// as it's the same expr-env, we need to detect here.
	if state.CurrentPhase != PhaseInBand {
		return errors.New("SendChallenge can only be called from an in-band hook (on_challenge or post_eval)")
	}

	// GrantChallengeCookie earlier in the same request already minted an
	// allowlist cookie; refuse to overwrite it with a challenge page.
	if state.ChallengeBypassed {
		w.Logger.Debugf("SendChallenge no-op: allowlist cookie already granted this request")
		return nil
	}

	// A hook flagged this request as exempt (verified bot, well-known path, ...).
	if state.ChallengeExempt {
		w.Logger.Debugf("SendChallenge no-op: request exempt from challenge")
		return nil
	}

	target := w.ChallengeRuntime.Difficulty()
	if state.ChallengeDifficulty != nil {
		target = *state.ChallengeDifficulty

View on GitHub (pinned to 909b515798)

Solutions

  1. Move the SendChallenge call into an in-band hook: inband.on_challenge or inband.post_eval
  2. If the intent is to block bot traffic in out-of-band processing, use a ban/drop remediation instead of SendChallenge
  3. Ensure the rule collection containing the call is only loaded under the in-band WAF component

Example fix

// before (out-of-band rule)
outofband:
  post_eval:
    - SendChallenge()

// after
inband:
  post_eval:
    - SendChallenge()
Defensive patterns

Strategy: validation

Validate before calling

if state.CurrentPhase != PhaseInBand {
    return fmt.Errorf("SendChallenge requires in-band phase; current phase: %s", state.CurrentPhase)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/829788cd3f4dd0e7. Report an issue: GitHub.