1-SEC contributors · Reviewed · Module: llm_firewall

Multi-Turn Prompt Injection: Session Heuristics and Escalation Signals

Input analysis scans supplied prompt text and passes the suspicious-result flag to a session tracker.

What 1-SEC checks

Input analysis scans supplied prompt text and passes the suspicious-result flag to a session tracker. The tracker compares suspicious turns in the first and second halves of its retained history, looks for increasing prompt length with repeated suspicion, and identifies ten recent prompts spanning less than thirty seconds. Source code starts a new session state after thirty minutes of inactivity and retains at most one hundred turn records.

Read the implementation: RecordAndAnalyze

Required telemetry and context

Relevant input events: llm_input, prompt, llm_request. Use a trusted producer at the real application boundary.

  • prompt
  • user_input
  • session_id
  • source_ip (fallback)
  • context
  • rag_context

Field names above describe relevant context, not a complete API request. Consult the current event contract for envelope and detail requirements. Preserve booleans, numbers, and strings as the receiving handler expects them. Agent-provided text cannot independently verify identity or authorization.

Deployment, prevention, and response

Assign session identifiers at the trusted application boundary and keep users and conversations distinct. Missing identifiers fall back to source IP, which can merge users behind a shared address. Compare clean conversations, gradual suspicious turns, and fast benign exchanges. Apply session authorization and interruption in the application; the tracker does not authenticate sessions or revoke tokens.

Record the source event, affected agent/host, actual target, policy decision, and response result. Review enforcement configuration separately from detection. A recommendation, selected action, dry-run record, and completed action are different outcomes.

Reproduce the source fixtures

From a checkout of the linked 1-SEC source revision, run the selected fixtures with the repository's Go toolchain. These commands run tests; they do not execute a real attack against your systems.

go test ./internal/modules/llmfirewall -run '^(TestMultiTurnTracker_NewSession|TestMultiTurnTracker_GradualEscalation|TestMultiTurnTracker_RapidFire|TestMultiTurnTracker_TurnHistoryCapped)$' -count=1

Selected source fixtures passed in the relevant local package suites. Read their exact assertions: helper state and string-pattern tests do not establish MCP transport conformance, framework interception, or production prevention.

TestMultiTurnTracker_NewSession

Full fixture and assertions — run in its original package with its helpers.

func TestMultiTurnTracker_NewSession(t *testing.T) {
	mt := NewMultiTurnTracker()
	result := mt.RecordAndAnalyze("session1", "Hello!", false)
	if result.TurnCount != 1 {
		t.Errorf("TurnCount = %d, want 1", result.TurnCount)
	}
	if result.SuspiciousTurns != 0 {
		t.Errorf("SuspiciousTurns = %d, want 0", result.SuspiciousTurns)
	}
}
TestMultiTurnTracker_GradualEscalation

Full fixture and assertions — run in its original package with its helpers.

func TestMultiTurnTracker_GradualEscalation(t *testing.T) {
	mt := NewMultiTurnTracker()
	sessID := "escalate"
	// First half: mostly clean
	for i := 0; i < 5; i++ {
		mt.RecordAndAnalyze(sessID, "normal turn", false)
	}
	// Second half: all suspicious
	for i := 0; i < 5; i++ {
		mt.RecordAndAnalyze(sessID, "attack attempt", true)
	}

	result := mt.RecordAndAnalyze(sessID, "final attack", true)
	if !result.GradualEscalation {
		t.Error("expected GradualEscalation=true when second half is more suspicious")
	}
}
TestMultiTurnTracker_RapidFire

Full fixture and assertions — run in its original package with its helpers.

func TestMultiTurnTracker_RapidFire(t *testing.T) {
	mt := NewMultiTurnTracker()
	sessID := "rapid"
	// 10 prompts sent very quickly (no sleep)
	for i := 0; i < 10; i++ {
		mt.RecordAndAnalyze(sessID, "quick probe", false)
	}
	result := mt.RecordAndAnalyze(sessID, "last probe", false)
	if !result.RapidFire {
		t.Error("expected RapidFire=true for 10+ prompts in rapid succession")
	}
}
TestMultiTurnTracker_TurnHistoryCapped

Full fixture and assertions — run in its original package with its helpers.

func TestMultiTurnTracker_TurnHistoryCapped(t *testing.T) {
	mt := NewMultiTurnTracker()
	sessID := "overflow"
	for i := 0; i < 120; i++ {
		mt.RecordAndAnalyze(sessID, "turn", false)
	}
	result := mt.RecordAndAnalyze(sessID, "next", false)
	// capped at 100 turns
	if result.TurnCount > 101 {
		t.Errorf("TurnCount=%d exceeds cap of 100", result.TurnCount)
	}
}

// ─── Tool Chain Monitor ───────────────────────────────────────────────────────

Tuning, evaluation, and coverage limits

This is a heuristic over text already submitted by an integration, not semantic recognition of every multi-turn attack. Rapid legitimate conversations can match. Suspicious-turn totals are maintained separately from the capped turn list. The inactivity branch is documented from source; the cited fixtures do not simulate a thirty-minute wait. Session state is bounded and does not persist across restart.

Evaluate this behavior with representative authorized activity and a controlled known-positive case. Investigate the exact rule, inputs, and state before suppressing an alert. Do not assume a hard-coded heuristic is a configurable setting. Measure your own deployment; the source fixtures do not establish a benchmark, detection rate, compliance certification, or incident-prevention claim.

Related implemented controls

Implementation reviewed at source revision fe508751a3f6d9bfe4b9bf8e6fa6b69e9b066771. Recheck the actual release and integration before deployment. Sources and corrections policy.