Workspace/Lesson workspace
Loading progress
Training & research45 min

Adversarial evaluation is a test of boundaries

Lesson 2 of 3
How to study this lesson

Read one section, trace the worked example, and try the knowledge check. Bookmark saves a shortcut; notes appear in My notebook. Mark lesson complete is your own assessment and does not mark its lab passed.

By the end, you can

  • Define attacker capabilities and authorized test scope.
  • Separate model instruction following from application authorization.
  • Measure attack outcomes and benign utility together.

Specify what the adversary can actually control

A useful threat model names the asset, the attacker, the entry point, the allowed manipulation, and the undesired outcome. An adversary who can edit a retrieved document has different capabilities from an authenticated user who can submit a prompt or an administrator who can change a tool implementation. Mixing them in one undifferentiated attack-success number hides which boundary failed.

For a retrieval assistant, define a test attacker who can alter the body of one synthetic document but cannot change authenticated identity, server-side permissions, or evaluation labels. The protected asset might be another tenant's records. The failure condition is unauthorized disclosure or action, not merely the model repeating suspicious text. Use only owned fixtures or environments where testing is authorized, with a defined request budget and cleanup procedure. This scope makes results interpretable as well as operationally safe.

Instructions are not a privilege boundary

Prompt injection occurs when untrusted content influences model behavior in an unintended way, including content received indirectly through retrieval or tool output. OWASP's GenAI guidance describes this risk and the need for layered mitigations. A system message asking the model to ignore malicious text can help behavior, but the application must still enforce authorization independently of the model's proposed action.

Imagine a retrieved page instructs the assistant to call export_records for tenant blue. The application should derive the requesting tenant from authenticated server context, validate the resource and scope, and reject a cross-tenant export even if the model confidently asks for it. The test should verify the downstream operation was blocked, not simply whether the assistant apologized. A model can say it refused while an earlier tool call already caused harm; conversely, suspicious generated text may be harmless if no privileged action or disclosure occurred.

Build cases that isolate a hypothesis

Vary one attack feature at a time when diagnosing a weakness. A document may contain a direct command, a forged system-message delimiter, a claim of administrator approval, or an instruction hidden in a structured field. Pair each adversarial fixture with a benign task using the same route. The comparison reveals whether a defense preserves legitimate retrieval and whether success depends on one formatting trick.

Use synthetic canaries and harmless local actions rather than real secrets or destructive operations. Save the exact fixture, model and prompt revision, tool schema, identity context, and observable result. A passing case should state which invariant held, such as "no cross-tenant record was returned." The same payload across many paraphrases is not necessarily broad coverage; vary the trust boundary, attack objective, and action pathway as well as the wording.

Define outcome metrics before inspecting outputs

Attack success rate requires a denominator: attempts, unique scenarios, prompts, or complete episodes. An episode with ten retries differs from one attempt, and the success probability can increase with the attacker's budget. Report the permitted number of attempts and the event that counts as success. Separate unauthorized tool execution, confidential data exposure, evaluator manipulation, and simple instruction-following deviations.

Also measure benign task completion and false denials. A defense that rejects every request can score perfectly on a narrow attack set while making the product useless. For an invented fixture set with 20 attack cases and 20 benign cases, zero unauthorized actions alongside only two benign completions is a very different result from zero unauthorized actions with 19 benign completions. These numbers illustrate metric interpretation and are not measured product-security claims.

Turn findings into reproducible regressions

For each observed failure, identify the weakest enforceable boundary: resource scoping, schema validation, egress policy, confirmation binding, output escaping, or tool-result handling. Fix the boundary and retain the minimized case as a regression test. Do not rely only on adding the exact attack string to a blocklist, because a paraphrase or alternate channel may express the same instruction. Record residual risk when the underlying model remains susceptible but a consequential action is contained.

MITRE ATLAS and OWASP can organize coverage and provide vocabulary, but a catalog match is not a proof that your deployment is secure. Revisit tests after changing tools, authentication, model providers, retrieval sources, or streaming behavior. A defensible red-team report explains what was tested, what happened, what control changed, and what remains outside scope. Its value is actionable evidence about the system, not a universal safety percentage.

Work through the code

The authenticated tenant is trusted input supplied by the application, while proposed_tenant represents an untrusted model proposal. This local policy simulation tests a narrow isolation rule. It does not implement authentication, retrieval, or an actual prompt-injection detector.

boundary_regression.py
python
def enforce_read(authenticated_tenant, proposed_tenant, document):
    if proposed_tenant != authenticated_tenant:
        return {"allowed": False, "content": None}
    if document["tenant"] != authenticated_tenant:
        return {"allowed": False, "content": None}
    return {"allowed": True, "content": document["content"]}

blue = {"tenant": "blue", "content": "CANARY_BLUE_7Q"}
red = {"tenant": "red", "content": "Red delivery status"}
cases = [
    ("cross-tenant proposal", "red", "blue", blue, False),
    ("mislabeled proposal", "red", "red", blue, False),
    ("permitted read", "red", "red", red, True),
]
for name, principal, proposed, document, expected in cases:
    result = enforce_read(principal, proposed, document)
    assert result["allowed"] is expected
    if not expected:
        assert result["content"] is None
    print(f"{name}: {'allowed' if result['allowed'] else 'denied'}")
EXPECTED / ILLUSTRATIVE OUTPUT
cross-tenant proposal: denied
mislabeled proposal: denied
permitted read: allowed

Run Python snippets locally with the prerequisites named above. The module coding lab runs directly in your browser.

Pause and reason

An agent prints "I cannot export that" after an export tool has already completed. A test checks only the final response and marks the attack blocked. Redesign the assertion.

Check your understanding

A defense blocks all 50 attack fixtures and all 50 benign requests. Which conclusion is supported?

Your notes

Explain the mechanism in your own words. Add a failure you want to test.

Saved notes appear in your notebook

Go deeper with primary sources

Practice this module