De-slop: remove every em-dash + banned words across all modules + capstone (#94)
Sync course wiki / sync-wiki (push) Successful in 4s

Co-authored-by: claude <claude@jpaul.io>
Co-committed-by: claude <claude@jpaul.io>
This commit was merged in pull request #94.
This commit is contained in:
2026-06-22 23:21:22 -04:00
committed by Claude (agent)
parent 513d7e7ac8
commit c098933f25
99 changed files with 1324 additions and 1315 deletions
@@ -1,4 +1,4 @@
# Module 22 Securing Third-Party MCP Servers and Skills
# Module 22: Securing Third-Party MCP Servers and Skills
> **Installing a third-party MCP server or skill means running untrusted code with access to your
> systems and data, and the AI driving it can be talked into turning that access against you.** Unit 4
@@ -8,20 +8,20 @@
## Prerequisites
- **Module 20 MCP Servers** — you've connected the AI to real tools and data over MCP. That
- **Module 20, MCP Servers.** You've connected the AI to real tools and data over MCP. That
connection is exactly the attack surface this module defends.
- **Module 21 Skills** — you've installed and authored skills (and seen that a skill is just
- **Module 21, Skills.** You've installed and authored skills (and seen that a skill is just
instructions plus, often, scripts the AI runs). A third-party skill is someone else's code and
someone else's instructions.
- **Module 15 Security Scanning for AI-Generated Code** Module 15 scans the code the AI *writes*.
- **Module 15, Security Scanning for AI-Generated Code.** Module 15 scans the code the AI *writes*.
This module secures the AI *as an actor*. Same instinct (automated gates against AI-shaped
failure), different target. The hallucinated-package supply-chain risk from Module 15 has a direct
cousin here.
- **Module 2 Version Control as a Safety Net** `git restore` and a clean commit are part of the
- **Module 2, Version Control as a Safety Net.** `git restore` and a clean commit are part of the
blast-radius story when something an agent did needs undoing.
- Helpful but not required: **Module 16** (containers, for sandboxing untrusted servers),
**Module 17** (secrets, for scoping the tokens you hand a server), and **Module 5** (committed
config your MCP/skill setup is itself a reviewable, versioned artifact).
config; your MCP/skill setup is itself a reviewable, versioned artifact).
---
@@ -29,8 +29,8 @@
By the end of this module you can:
1. Name the four new attack surfaces an MCP server or skill adds prompt injection, tool/agent
abuse, over-broad permissions, and the supply chain and explain why each is *AI-specific*.
1. Name the four new attack surfaces an MCP server or skill adds (prompt injection, tool/agent
abuse, over-broad permissions, and the supply chain) and explain why each is *AI-specific*.
2. Reproduce a prompt-injection attack: get an agent to act on malicious instructions smuggled in
through content it merely read, not content you typed.
3. Audit a third-party MCP server or skill against a concrete checklist *before* you install it, and
@@ -59,10 +59,10 @@ from a random repo exactly the same way.
There are four distinct surfaces. Keep them separate in your head; the defenses differ.
### Surface 1 Prompt injection (the one that's genuinely new)
### Surface 1: Prompt injection (the one that's genuinely new)
Classic security assumes code and data are separate: code is trusted, data is inert. LLMs erase that
line. To a model, **everything is text in the same context window** your instructions, the tool
line. To a model, **everything is text in the same context window**: your instructions, the tool
output, the file it read, the issue someone else filed. There is no reliable boundary between "what
the user told me to do" and "words that happened to appear in the data I was told to look at." So an
attacker who can get text in front of the model can try to issue it instructions.
@@ -93,7 +93,7 @@ malicious word. You asked it to read your issues.
Injection text doesn't have to be visible, either. It hides in HTML comments on a web page the agent
fetches, in white-on-white text in a PDF, in a commit message, in the description field of an MCP
tool the server advertises (a *tool-description* injection the malicious instruction is in the
tool the server advertises (a *tool-description* injection, where the malicious instruction is in the
server's own metadata), even in zero-width Unicode characters inside a file. Anywhere the model
reads, an attacker can try to write.
@@ -103,7 +103,7 @@ injection overrides). Injection is mitigated *architecturally*, by limiting what
allowed to do once it has been exposed to untrusted content, not by cleverness. That's why the rest
of this module is about permissions, not prompts.
### Surface 2 Tool and agent abuse
### Surface 2: Tool and agent abuse
Even without a planted attacker, a tool can be invoked in ways you didn't intend. A "run SQL"
MCP server given write credentials can `DROP TABLE` when the model misreads a request. A "send
@@ -122,7 +122,7 @@ the credentials to your customer database *and* an outbound HTTP tool. Split cap
agents, or drop a leg (read-only DB, no outbound network, no untrusted input on the privileged
agent).
### Surface 3 Over-broad permissions
### Surface 3: Over-broad permissions
This is the boring one that does the most damage, because it's the *default*. An MCP server's setup
docs say "create a token," so you create a token with every scope, because that's the path of least
@@ -144,10 +144,10 @@ The fixes are ordinary least-privilege, applied to a new kind of consumer:
(Module 16) with no host filesystem, a dropped network, and no ambient cloud credentials than it
does as your user with your `~/.aws` mounted.
### Surface 4 The MCP-and-skills supply chain
### Surface 4: The MCP-and-skills supply chain
A skill or MCP server you install from a registry, a gist, or a "awesome-mcp" list is a dependency,
and it carries every supply-chain risk Module 15 taught plus a new one. The Module 15 cousin:
and it carries every supply-chain risk Module 15 taught, plus a new one. The Module 15 cousin:
attackers register **plausible-but-fake** server and skill names (typosquats of popular ones, or the
name an LLM would *guess* when you ask it to "install the GitHub MCP server"). You ask your agent to
set it up, it picks a malicious lookalike, and you've installed an attacker's code.
@@ -176,7 +176,7 @@ gates on dangerous actions, and a clean checkpoint to restore to. That's the pos
## The AI angle
Every other security module in this course defends against *code*. This one defends against an
*actor* a capable, eager, literal-minded actor that reads attacker-controlled text as readily as
*actor*: a capable, eager, literal-minded actor that reads attacker-controlled text as readily as
it reads yours and cannot reliably tell the difference. That's the specific thing that makes MCP and
skills different from any dependency you've shipped before:
@@ -186,8 +186,8 @@ skills different from any dependency you've shipped before:
- The supply-chain risk isn't just "malicious package." It's "malicious *instructions*," which can
arrive after install, through data, from a third party who never touched your dependency tree.
- And the mitigation is unusually un-clever: no prompt, no model upgrade, no smarter system message
fixes injection. The defenses are the oldest ones in security least privilege, isolation,
separation of duties, human approval on irreversible actions which is exactly why an IT pro is
fixes injection. The defenses are the oldest ones in security (least privilege, isolation,
separation of duties, human approval on irreversible actions), which is exactly why an IT pro is
the right person to apply them. You already know this playbook. Unit 4 just gave you a new thing to
point it at.
@@ -203,7 +203,7 @@ against the Module 1 `tasks-app` and apply the least-privilege mitigation.
Python 3.10+, and your AI agent (the examples use Claude Code; sub your own). The lab files live in
this module's folder at `~/ai-workflow-course/modules/22-securing-third-party-mcp-and-skills/lab/`.
### Part A Vet a third-party skill before you install it
### Part A: Vet a third-party skill before you install it
In `suspicious-skill/` (under the lab folder) is a skill called `notion-task-export` that claims to
"export your tasks to Notion." It's the kind of thing you'd find on an "awesome skills" list.
@@ -224,29 +224,29 @@ it. This is the artifact to audit, not something to install.
`audit.sh` is a concrete, runnable version of the vetting checklist. It flags: outbound network
calls, reads of credentials and env vars, shell-out / `eval` / `exec`, broad filesystem access
(`~/.ssh`, `~/.aws`, home dir), `curl | bash` patterns, and **hidden instructions** including
(`~/.ssh`, `~/.aws`, home dir), `curl | bash` patterns, and **hidden instructions**, including
zero-width Unicode planted in the Markdown to smuggle a directive past a human reader. Read its
output against the source.
3. **Score it against the checklist** (this is the deliverable answer each, out loud or in notes):
3. **Score it against the checklist** (this is the deliverable; answer each, out loud or in notes):
- [ ] **Provenance** — who publishes it? First-party (the vendor whose API it uses) or a random
- [ ] **Provenance.** Who publishes it? First-party (the vendor whose API it uses) or a random
account? How many maintainers, how much history? (For the lab, treat it as `random-user`.)
- [ ] **Claim vs. behavior** — does the code do only what the description says? (It doesn't.)
- [ ] **Permissions requested** — what credentials, scopes, paths, and hosts does it touch? Are
- [ ] **Claim vs. behavior.** Does the code do only what the description says? (It doesn't.)
- [ ] **Permissions requested.** What credentials, scopes, paths, and hosts does it touch? Are
any broader than the stated job needs?
- [ ] **Network egress** — where does it send data, and is that endpoint the one it claims?
- [ ] **Hidden instructions** — any injected directives in the writing, comments, or invisible
- [ ] **Network egress.** Where does it send data, and is that endpoint the one it claims?
- [ ] **Hidden instructions.** Any injected directives in the writing, comments, or invisible
characters?
- [ ] **Pinning** — can you pin a reviewed version, or does it auto-update into your trust
- [ ] **Pinning.** Can you pin a reviewed version, or does it auto-update into your trust
boundary?
- [ ] **Verdict** — install, install-with-changes (scoped/sandboxed), or reject?
- [ ] **Verdict.** Install, install-with-changes (scoped/sandboxed), or reject?
The correct verdict here is **reject** `sync.py` exfiltrates environment variables to an
The correct verdict here is **reject**: `sync.py` exfiltrates environment variables to an
attacker host, and `SKILL.md` hides an instruction telling the agent to include `.env` contents.
You caught it before it ran. That's the whole skill.
### Part B Reproduce a prompt injection, then break it with least privilege
### Part B: Reproduce a prompt injection, then break it with least privilege
Now feel the attack the checklist exists to stop. You'll act as both the victim (you ask your agent a
normal question) and the attacker (you plant content the agent reads).
@@ -270,9 +270,9 @@ normal question) and the attacker (you plant content the agent reads).
partly comply (acknowledge the "system note," change its behavior, or follow the embedded
instruction). **Either way, you just handed the model attacker-controlled text and asked it to act
on a context that contained an instruction you didn't write.** That's the entire mechanism. In a
real setup the agent reads that task list *itself* via an MCP server you'd never see the payload.
real setup the agent reads that task list *itself* via an MCP server, and you'd never see the payload.
3. **Apply the mitigation architecture, not wording.** You can't reliably prompt the injection
3. **Apply the mitigation: architecture, not wording.** You can't reliably prompt the injection
away. Instead, remove the legs of the trifecta and gate the dangerous actions. Write down, for the
"agent that reads my tasks" scenario, the least-privilege design:
@@ -285,7 +285,7 @@ normal question) and the attacker (you plant content the agent reads).
- **Human gate on writes:** any tool that mutates state is confirm-first, so the model can't
irreversibly act on smuggled instructions without you seeing the call.
- **Treat tool output as data:** in your committed config (Module 5), instruct the agent to treat
file/issue/tool content as information to *report on*, never as commands to follow — knowing
file/issue/tool content as information to *report on*, never as commands to follow. Know
this is a speed bump, not a wall, which is why the structural controls above carry the load.
4. **Prove the read-only leg.** Confirm the mitigation isn't hypothetical: if your task server is
@@ -295,7 +295,7 @@ normal question) and the attacker (you plant content the agent reads).
```bash
# the "tool" the agent is allowed to call in read-only mode
python cli.py list # works
# the tool it is NOT exposed (a write) in a least-privilege setup this path is simply absent
# the tool it is NOT exposed (a write); in a least-privilege setup this path is simply absent
```
Then clean up the planted attack state so your repo is honest again. Don't decide-and-delete by
@@ -315,13 +315,13 @@ normal question) and the attacker (you plant content the agent reads).
## Where it breaks
- **You cannot fully solve prompt injection.** Anyone selling you a prompt, a guardrail model, or a
"secure mode" that *eliminates* it is overselling. State of the art is *reduction* input
"secure mode" that *eliminates* it is overselling. State of the art is *reduction*: input
filtering catches known patterns and raises the bar, but the only durable defense is limiting blast
radius. Design as if injection will eventually succeed.
- **Least privilege fights usefulness.** A locked-down agent is a less capable agent. Read-only,
no-network, human-gated tools are safer and slower, and people route around friction. The honest
answer is to match privilege to stakes: tight by default, loosened deliberately for specific,
reviewed workflows not loosened everywhere because the demo was annoying.
reviewed workflows, not loosened everywhere because the demo was annoying.
- **`audit.sh` is a smoke detector, not a guarantee.** Static red-flag scanning catches the obvious
and the lazy. It does not catch obfuscated payloads, logic that only misbehaves under certain
inputs, or a clean v1 that turns malicious in v2. Reading the code and pinning the version still
@@ -330,7 +330,7 @@ normal question) and the attacker (you plant content the agent reads).
version is unreviewed code with your reviewed reputation attached. Auto-update quietly voids your
audit. Pin, and re-vet on bump.
- **Sandboxing has seams.** A container (Module 16) contains a misbehaving server far better than
running it as your user but mounted volumes, forwarded credentials, and host networking are holes
running it as your user, but mounted volumes, forwarded credentials, and host networking are holes
you can punch right back through. Isolation only helps to the extent you don't undo it for
convenience.
@@ -345,13 +345,13 @@ normal question) and the attacker (you plant content the agent reads).
- You can name the four attack surfaces (prompt injection, tool/agent abuse, over-broad permissions,
supply chain) and give a one-line example of each.
- You reproduced the prompt injection against `tasks-app` and watched the model act on text you
didn't type and you can explain why a better prompt is *not* the fix.
didn't type, and you can explain why a better prompt is *not* the fix.
- You can describe the lethal trifecta and how to break it for a real agent you'd actually run, and
you can write a least-privilege setup (scoped token, read-only default, allowlisted paths/hosts,
pinned version, human gate on writes) for one MCP server or skill from your own work.
When "should I install this MCP server?" triggers the same reflex as "should I pipe this script into
a root shell?" and you have a checklist for both you've got it. Module 23 turns the
a root shell?", and you have a checklist for both, you've got it. Module 23 turns the
extend-the-AI toolkit on the hardest target: a large codebase you didn't write.
---
@@ -360,18 +360,18 @@ extend-the-AI toolkit on the hardest target: a large codebase you didn't write.
Expansion-zone module; the surface this defends moves fast. Re-check at build time:
- [ ] **Injection mitigations** — is "no model is immune; mitigate architecturally" still the
- [ ] **Injection mitigations.** Is "no model is immune; mitigate architecturally" still the
consensus? If a genuinely effective input-level defense has emerged, note it *as a layer*, not
as a solution, and keep the least-privilege spine.
- [ ] **The lethal-trifecta framing** — still the common shorthand (private data + untrusted content
- [ ] **The lethal-trifecta framing.** Still the common shorthand (private data + untrusted content
+ external comms)? Keep the attribution-free, descriptive phrasing; update if terminology has
shifted.
- [ ] **MCP permission controls** — do current MCP clients/servers still support per-tool exposure,
- [ ] **MCP permission controls.** Do current MCP clients/servers still support per-tool exposure,
read-only modes, and per-call human approval? Update the wording if the common mechanisms have
moved (e.g., signed servers, registries with provenance, OAuth scoping baked into the protocol).
- [ ] **Supply-chain tooling** — has a trustworthy MCP/skill registry with provenance or signing
- [ ] **Supply-chain tooling.** Has a trustworthy MCP/skill registry with provenance or signing
become standard? If so, fold "prefer signed/registry sources" into Surface 4.
- [ ] **Typosquat/hallucinated-name risk** — confirm the Module 15 cross-reference still holds and
- [ ] **Typosquat/hallucinated-name risk.** Confirm the Module 15 cross-reference still holds and
the named threat (LLMs guessing plausible-but-fake server/skill names) is still current.
- [ ] `bash audit.sh suspicious-skill` (run from the lab folder) still flags the network egress,
env-var read, and hidden-Unicode instruction, and the `tasks-app` injection lab still works