Frontend Labs
⋯

Chapter 61 — Securing prompts, context, and tools

In one sentence

Treat the prompt as configuration, the context as untrusted input, credentials as never-in-the-context, and every tool as an API endpoint exposed to the internet.

Securing the prompt

Your system prompt is not a secret. Users can usually extract it, and "prompt leaking" attacks are trivial. Design accordingly:

   NEVER put in a system prompt:
     * API keys, passwords, tokens, connection strings
     * internal URLs or hostnames you don't want known
     * customer data
     * "security rules" you rely on for actual security

   DO put in a system prompt:
     * role, tone, and format instructions
     * scope ("only answer questions about our products")
     * an escape hatch ("if you don't know, say so")
     * safety guidance -- as a FIRST layer, not the only one

Version and test prompts like code. They're in the release; they belong in git, in code review, and in your eval suite (Chapter 49). A one-word prompt change can alter behaviour more than a model upgrade.

Securing the context

Everything entering the context window is potentially hostile. Classify each source:

   TRUSTED    your system prompt, your own configuration
   SEMI       the authenticated user's own message
              (they can attack themselves; they should not be able to
               attack the SYSTEM or other users)
   UNTRUSTED  retrieved documents, web pages, emails, tool results,
              file contents, third-party MCP servers, other users' data

For everything untrusted:

  1. Sanitise — strip special tokens (Chapter 30) and invisible Unicode (zero-width, bidi override, tag characters).
  2. Delimit and label — mark it explicitly as data with its source.
  3. Cap the size — a 2 MB document is a cost attack and a context overflow.
  4. Isolate per user — scope caches, memory, and vector stores per tenant. A shared cache that returns one user's answer to another is a serious, common bug.
  5. Filter what goes out — apply your retrieval permission checks before the search, not after (post-filtering leaks existence).
def retrieve(query, user):
    # RIGHT: filter by permission in the query
    return vector_db.search(query, filter={"tenant": user.tenant_id,
                                           "acl": {"$in": user.groups}})
    # WRONG: search everything, then drop what they can't see
    #        (still leaks counts, ranking, and sometimes snippets)

Securing credentials

The model must never see a credential. Not in the prompt, not in a tool result, not in an error message.

   BAD:   tool returns {"error": "psql: FATAL: password authentication failed
                                  for user 'admin' with password 'hunter2'"}
   GOOD:  tool returns {"error": "Database unavailable."}   (details go to YOUR logs)

Practices:

  • Credentials live in your execution layer, injected by your code when it calls the real API. The model supplies business arguments only.
  • Short-lived, scoped tokens — per-user, per-purpose, minimal scope.
  • Redact tool outputs before they enter the context.
  • Rotate anything that may have appeared in a log or a context window.

Securing tools

Treat every tool as a public API endpoint that a motivated attacker can call with arbitrary arguments — because effectively, it is.

   [ ] Narrow scope   -- get_order(id), NOT run_sql(query)
   [ ] Schema-validate every argument (types, ranges, patterns, enums)
   [ ] Authorise using session identity, never model-supplied identity
   [ ] Enforce limits in code (amounts, counts, rates)
   [ ] Read-only by default; writes require approval
   [ ] Idempotency keys on anything that could be retried
   [ ] Timeouts on every call
   [ ] Truncate and sanitise results before they re-enter the context
   [ ] Log every invocation with the trace ID (Chapter 55)

Code execution deserves its own rules. If you let a model run code:

   [ ] a container or microVM, never your host
   [ ] NO network, or a strict allowlist
   [ ] NO credentials mounted, no cloud metadata endpoint access
   [ ] ephemeral filesystem, destroyed after the run
   [ ] CPU, memory, and wall-clock limits
   [ ] never eval() in your own process

(The eval in Chapter 51's practice was a deliberately-flagged teaching shortcut. Don't ship it.)

Securing MCP specifically

Control Why
Vet servers before installing Same as any dependency — read the source, check the publisher, pin the version
Pin versions; review updates A benign server can turn malicious in an update (supply chain)
Treat tool descriptions as untrusted The spec says so explicitly. They enter the model's context.
Re-check descriptions between sessions A server can change them; scan for changes (mcp-scan)
Run servers with least privilege A stdio server inherits your permissions — give it a dedicated account and a scoped directory
OAuth 2.1 for remote servers PKCE required, no implicit grant; validate the token audience so you only accept tokens minted for you
Don't pass user tokens through Avoid the confused deputy: the server should act with its own scoped identity, not a borrowed one
Network-isolate remote servers Egress allowlist; no access to internal metadata services

The checklist before you ship

   [ ] Every tool: least privilege, validated arguments, session-based identity
   [ ] Irreversible actions require human approval
   [ ] Budget, step, and rate caps enforced in code
   [ ] Outbound requests restricted to an allowlist
   [ ] No credentials reachable from the model or from executed code
   [ ] Untrusted content sanitised, delimited, and size-capped
   [ ] Per-user isolation of memory, cache, and retrieval
   [ ] Full audit log with trace IDs
   [ ] Adversarial tests (injection, over-limit, cross-user) in CI
   [ ] A kill switch: one flag that disables tools instantly
   [ ] An incident plan: what to do when it does go wrong

That last pair matters. You will have an incident. Being able to turn tools off in one action, and knowing who does what, is the difference between a five-minute problem and a five-day one.

Practice (40 min)

A. Audit your agent against the checklist above. Score yourself honestly.

B. Add a kill switch — an environment variable or config flag that makes execute_tool refuse everything. Test it works.

C. Write the adversarial suite (five tests minimum):

  1. Injection in a retrieved document
  2. Injection in a tool's result
  3. A request exceeding a policy limit
  4. An attempt to access another user's data
  5. An attempt to make the agent fetch an off-allowlist URL

Run them in CI. A guardrail without a test is a hope.

D. Try to break your own agent for fifteen minutes. Write down what you tried and what happened. Then fix the gaps you found.

Common confusions

  • "My prompt says not to reveal the system prompt, so it's protected." It isn't. Assume it's public.
  • "I'll encrypt the system prompt." The model must read it in plaintext to use it. There's nowhere to hide it.
  • "Sandboxing is overkill for internal tools." Internal agents read external content. That's the whole attack surface.
  • "We'll add security later." Retrofitting least privilege into an agent that already has broad credentials is far harder than starting narrow.

Check yourself

  1. Why should you assume your system prompt is public?
  2. Name three ways to sanitise untrusted context.
  3. Why must permission filtering happen before retrieval?
  4. What does audience validation prevent in OAuth?
  5. What is a kill switch and why do you need one?

Further reading

  • Docs: OWASP MCP Security Cheat Sheet and OWASP Top 10 for LLM Applications.
  • Docs: modelcontextprotocol.io — Security Best Practices, and the Authorization section of the specification.
  • Tool: mcp-scan (Invariant Labs) — scan MCP servers for poisoned tool descriptions.
  • Blog: Simon Willison's prompt-injection archive — ongoing, practical, and honest about what doesn't work.

End of Part 14 — Milestone check

  • I can explain why instructions and data can't be separated in an LLM
  • I have demonstrated indirect prompt injection on my own agent
  • I can name the three guardrail layers and say which matters most
  • I can explain why policy belongs in code, not the prompt
  • I have added action guardrails and an approval queue to my agent
  • I have an adversarial test suite running in CI
  • My agent has a kill switch

Generated from real-life-ai-example-v1.md · 8 Oct 2026 16:36