Sections in this lesson

Direct & Indirect Prompt Injection

Direct input, a retrieved document, a tool result or repository text may contain instructions that conflict with the task.

Reviewed: · Content owner: Interview Preparator

What you will learn

  • Untrusted text is not authority
  • Constrain effects instead of trusting refusal
  • Use controlled evaluations

Before you start

The flow

  1. 1.Identify scope
  2. 2.Inspect evidence
  3. 3.Apply host control
  4. 4.Run negative cases
  5. 5.Compare benign behavior
  6. 6.Review remaining risks

Untrusted text is not authority

Direct input, a retrieved document, a tool result or repository text may contain instructions that conflict with the task. Injection redirects behavior; a jailbreak concerns bypassing a model behavior restriction. Either can affect an application, but they are not synonyms.

Mark each input origin and keep privileged decisions in the host. A system prompt can describe policy, yet cannot authenticate a caller or prevent an already privileged process from sending data.

Constrain effects instead of trusting refusal

The fixture proposes unsupported tool actions such as shell or export. The host rejects the action schema before any effect; it never exposes those capabilities. This demonstrates the application boundary, not a classifier detecting every malicious sentence.

Compare allowed read operations with denied proposals. A defense that rejects all requests can look secure while being unusable. Keep benign task success next to attack outcomes.

Use controlled evaluations

Injected fixtures contain synthetic text and no real secrets. Inspect attempted actions and resource scopes, not just the final response. No external attacker endpoint is contacted by this lab.

Real model resistance requires adversarial natural-language cases, repeated inference and independent annotation. A prompt change that improves one fixture may fail another; version policy, prompt, tools and corpus together.

Try it

host.mjs
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { authorize, payloadDigest } from './policy.mjs';
// Teaching host: no shell, network, or arbitrary-file tools are exposed.
export class TeachingHost {
constructor({ scope, resource, grant, maxCalls = 3, maxTokens = 100, deadline = 1000, epoch = 1 }) {
if (!Number.isSafeInteger(maxCalls) || maxCalls < 1 || !Number.isSafeInteger(maxTokens) || maxTokens < 1 || !Number.isFinite(deadline)) throw new Error('Invalid budget');
this.scope = Object.freeze({ ...scope }); this.resource = structuredClone(resource); this.grant = grant ? structuredClone(grant) : null;
this.maxCalls = maxCalls; this.maxTokens = maxTokens; this.deadline = deadline; this.epoch = epoch;
this.calls = 0; this.tokens = 0; this.mutations = 0; this.receipts = new Map(); this.trace = [];
}
execute(call, callId, reservedTokens, now) {
if (!Number.isSafeInteger(reservedTokens) || reservedTokens < 1 || !Number.isFinite(now) || !/^[a-z0-9-]{1,80}$/.test(callId)) throw new Error('Invalid operation');
if (now >= this.deadline) return this.record('TIMEOUT');
const permission = authorize(this.scope,call,this.resource,this.grant,now,this.epoch);
if (permission !== 'ALLOW') return this.record(permission);
const digest = payloadDigest(call);
const cached = this.receipts.get(callId);
if (cached) return this.record(cached.digest === digest ? 'REPLAY' : 'PAYLOAD_MISMATCH');
if (this.calls >= this.maxCalls || reservedTokens > this.maxTokens - this.tokens) return this.record('BUDGET_EXCEEDED');
this.calls++; this.tokens += reservedTokens;
if (call.action === 'completePlan') this.mutations++;
this.receipts.set(callId,{ digest });
return this.record('EXECUTED');
}
record(status) { this.trace.push({ status, calls:this.calls, tokens:this.tokens, mutations:this.mutations }); return status; }
stopAndRevoke() { this.epoch++; this.grant = null; }
}
// Cache keys include authorization scope and corpus version; cache values still
// require current authorization at read time (revocation/deletion can change).
export function cacheKey(scope, corpusVersion, question) { return JSON.stringify([scope.tenant,scope.user,corpusVersion,question]); }
export function memoryRead(record, scope, version) {
return record.tenant === scope.tenant && record.user === scope.user && record.conversation === scope.conversation
&& record.trusted === true && record.version === version ? record.text : null;
}

Run npm run examples:security and npm run eval:security. This source is a deterministic teaching fixture; it does not prove live model safety, NLI, production auth or OS/network isolation.

Interview questions

Is prompt injection identical to a jailbreak?

Mark each input origin and keep privileged decisions in the host. A system prompt can describe policy, yet cannot authenticate a caller or prevent an already privileged process from sending data.

Why inspect effects instead of refusal text?

Compare allowed read operations with denied proposals. A defense that rejects all requests can look secure while being unusable. Keep benign task success next to attack outcomes.

What must a live injection evaluation add?

Real model resistance requires adversarial natural-language cases, repeated inference and independent annotation. A prompt change that improves one fixture may fail another; version policy, prompt, tools and corpus together.

Practice

Extend one negative scenario and its authorized positive counterexample for Direct & Indirect Prompt Injection.

Check your solution

  • State the trusted input and effect boundary.
  • Assert the expected status and observed effect.
  • Report coverage and limitations without claiming production readiness.

Official references

Continue learning

Category sequence

AI Security

Lesson 2 / 14

Was this lesson useful?

Change notes: 2026-10-31: security/reliability curriculum and source-bound fixture.