Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

allow-list
A list of what is permitted, with everything not on it refused by default.
fail closed
Behaving so that an error blocks the action rather than letting it through unchecked.
false refusal
A refusal of a request that was actually allowed, which costs a real user an answer.
incident
An event where the system did something harmful or outside policy, handled from detection to review.
least privilege
Giving each part of the system only the access it needs for its current job, and no more.
moderation
Checking content against rules about what is acceptable, and blocking or flagging what is not.
normalisation
Rewriting input into one standard form, such as lower case or expanded characters, before checking it.
PII
Information that can identify a person, alone or combined with other details, such as a name or address.
policy
A written rule that says what the system must allow, block or escalate, kept in code with a change history.
prompt injection
Text in input or retrieved content that tries to make the system follow the instructions of an attacker instead of yours.
provenance
The record of where a piece of content came from and how it reached the system.
red team
People or scripts that attack the system on purpose to find failures before real users do.
refusal
A reply that declines a request and says what the system can do instead, without revealing protected details.
threat model
A written list of what you are protecting, who might attack it and how, made before you build.

Guardrails and safety

Output validation and moderation

The model’s output is untrusted input to the next system. Validate the shape, moderate the content, and never render, fetch or execute what you did not check.

Part 4 of 10

Intermediate15 min readBack to Courses

Output is input

The model’s output leaves the model and becomes input to something else: a browser, a shell, a SQL client, another model, a colleague’s inbox. Every one of those has its own injection surface. Markdown that renders HTML is cross-site scripting; a URL fetched by a server is SSRF; a string concatenated into a query is SQL injection; a command run with the user’s privileges is remote code execution.

The mental model that works is a form submission from an untrusted client. You would not render a stranger’s HTML, follow a stranger’s URL or execute a stranger’s command because it arrived in a JSON field. The model is that stranger, however helpful its tone.

The sink matters as much as the text. The same summary is harmless in a log and dangerous in innerHTML, harmless in a chat window and dangerous in a shell. Classify the sink before you decide how much to trust the output, because the sink is what turns text into an effect.

Shape first

Validate the shape before the content. A schema check catches the wrong type, the missing field, the unexpected property and the value outside an enum, and it does so deterministically. Parse into a typed object, not a dictionary you index later. Then check the domain: the id exists, the amount is within the cap, the date is in the future.

When validation fails, you have three options: repair, retry or refuse. Repair means a deterministic fix such as trimming or filling a default; retry means asking the model again with the error message; refuse means stopping. Retry once, then refuse. A loop that retries until the JSON parses is a denial of wallet with a nice name.

Structured output helps, but it is not validation. A model can return valid JSON with a negative quantity or an id from another tenant; the schema only says the shape is right. The domain check is still yours, and it still runs after the parse.

Parse, then sanitise

import bleach
from pydantic import BaseModel, Field, ValidationError


class Answer(BaseModel):
    summary: str = Field(max_length=800)
    confidence: float = Field(ge=0, le=1)
    sources: list[str] = Field(max_length=5)


try:
    answer = Answer.model_validate_json(raw)
except ValidationError as error:
    print(error.errors())
    answer = None

if answer is not None:
    safe = bleach.clean(answer.summary, tags=[], strip=True)

Validate, retry once, refuse

Run the three payloads. Then add an enum for sources and a payload with an unknown source, and confirm the retry carries a message the model could act on.

Output
Click "Run" to execute your code

Moderation is a classifier, not a judge

A moderation model returns a score per category. It is a signal with a threshold, not a verdict. Choose thresholds per category and per product: a children’s app and a security research tool should not share one. Measure the false-positive rate on benign traffic, because over-blocking is the failure mode users notice first.

Keep the decision record: category, score, threshold, action. That is what lets you explain a block and tune it later. Never treat the moderator’s label as the policy itself; the policy is yours, and the classifier is one input to it.

Moderation also has a language problem. A classifier trained mostly on English will miss the same content in another language, and the gap is invisible in an aggregate score. Measure per locale, or the average will hide the users you are failing.

Validate before you render, not after. A sanitiser that runs after the browser has parsed the HTML has already lost the argument.

URLs, HTML and code

URLs need an allow-list of schemes and hosts. https only, no userinfo, no redirects to a private address, and a DNS check at fetch time rather than at validation time. HTML needs a sanitiser with an allow-list of tags, not a block-list of script tags. Code needs a sandbox, not a regex.

The same rule applies to file paths and shell arguments: build them from a known base and a validated component, never by concatenation. If the model supplies a path, resolve it and check that the result is still inside the directory you meant.

For code, the only safe default is not to run it. If the product needs execution, use a sandbox with no network, a time limit and a disposable filesystem, and treat the sandbox as the control rather than the prompt. A regex is not a jail.

Plain text and allowed URLs

Run the samples and find the URL that contains the allowed host as text but does not point at it. Notice that stripping tags still leaves the script text, then add a check that rejects a URL with a fragment and one with an explicit port.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the right mental model for model output?