|
|
|
@@ -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
|
|
|
|
|