DEV Community

Edison Flores
Edison Flores

Posted on

I built 8 security layers for an MCP marketplace. Here's what each one actually catches.

After a real trojan slipped through my MCP marketplace last week (Trojan:Win64/Lazy.PGPK!MTB hidden in a nested zip), I went deep on defense-in-depth. The result is 8 layers running in production at marketnow.site.

Here's what each layer actually catches — with concrete examples.

Layer 1: L1.5 — Metadata checks (6 rules)

The cheapest layer. Runs on every skill's metadata (name, description, system_prompt, install command).

Catches:

  • Skills that say "no auth required" (warning)
  • Skills with prompt injection patterns in descriptions ("ignore previous instructions")
  • Skills with file/SQL/HTTP access declared in metadata
  • Skills with Access-Control-Allow-Origin: *
  • Skills using unscoped OAuth tokens
  • Skills with no rate limiting

Doesn't catch: anything inside the actual package zip. That's why the trojan got through initially.

Layer 2: L1.6 — Semgrep + Secrets + OSV (36 rules)

18 Semgrep-equivalent rules + 18 secret detection patterns + OSV dependency vulnerability check.

Catches:

  • Hardcoded API keys (Stripe sk_live_*, GitHub ghp_*, AWS AKIA*)
  • Wallet mnemonics in descriptions
  • Command injection patterns (exec(req.body))
  • SSRF patterns (fetch(req.url))
  • Path traversal (readFile(req.params.path))
  • Tool name spoofing (a tool calling itself read_file to impersonate the official one)
  • Known vulnerable npm dependencies (via OSV API)

Doesn't catch: secrets inside code blocks in README (we strip those — false positives), process.env.X references (variable lookups, not hardcoded).

Layer 3: L1.7 — Binary & malware detection (8 patterns)

This is the layer I built after the trojan incident. It opens the actual package zip (recursively — zips inside zips) and scans for:

  • Windows binaries (.exe, .dll, .scr, .msi) → instant quarantine
  • Launcher scripts (.bat, .cmd, .vbs, .ps1) → instant quarantine
  • Nested archives (zips inside zips — legit MCP skills don't do this)
  • Staged launchers (start X.exe Y.txt pattern — the exact prospector trojan signature)
  • Obfuscated Lua bytecode (high-arity function signature function(o,R,F,U,b,p,E,M,Z,W,...))
  • External download URLs in README (raw.githubusercontent.com/.../...zip)
  • PowerShell -encodedcommand with long base64
  • eval(atob(...)) obfuscation
  • Oversized text files (>100KB non-JSON = likely bytecode payload)

Catches: the exact trojan that hit us. Verified with a smoke test that scans the original malicious zip from git history.

Layer 4: L1.8 — Malware family signatures (17 families)

YARA-equivalent rules for specific malware families:

  • Win64/Lazy.PGPK (the one that hit us)
  • Emotet (banking trojan)
  • Cobalt Strike (post-exploitation beacon)
  • Mimikatz (credential dumper)
  • QakBot, TrickBot (banking trojans)
  • Agent Tesla (keylogger)
  • RedLine, Vidar, Raccoon, LummaC2 (stealers)
  • AsyncRAT, njRAT, Remcos (RATs)
  • SolarMarker (backdoor)
  • Lokibot (credential stealer)
  • DoS tools (hping3, slowloris, goldeneye)

Each rule has a MITRE ATT&CK technique ID. Any match → instant quarantine.

Layer 5: WAF — Web Application Firewall (40 rules)

Inspects every incoming HTTP request for attack patterns:

  • SQLi (7 rules): UNION SELECT, OR 1=1, stacked queries, time-based, information_schema
  • XSS (7 rules): script tags, event handlers, javascript: URIs, img onerror, svg onload
  • Path traversal (5 rules): ../, encoded %2e%2e, /etc/passwd, /proc/self, Windows paths
  • SSRF (6 rules): AWS/GCP/Azure metadata IPs, file://, gopher://, dict://
  • Command injection (5 rules): backticks, $(), chained ; ls, pipe | cat, && ||
  • NoSQL injection (3 rules): $where, $ne, $gt
  • Prototype pollution: __proto__, constructor.prototype
  • SSTI: Jinja2 {{ }}, Twig {% %}, JS ${ }
  • Log injection: with header injection

Auto-ban after 5 WAF hits in 10 minutes (1-hour ban).

Layer 6: Honeypot (50+ paths)

Fake vulnerable paths that auto-ban scanners for 24 hours:

  • /.env → serves a fake env file with canary tokens
  • /admin → serves a fake admin login form
  • /wp-admin → serves a fake WordPress login
  • /.git/config → serves a fake git config
  • /.aws/credentials → serves fake AWS credentials
  • /.ssh/id_rsa → serves a fake SSH key
  • /phpmyadmin → serves a fake phpMyAdmin
  • /backup.sql → serves a fake database dump
  • /server-status, /.DS_Store, /web.config, /Dockerfile, etc.

Any access → IP banned 24h + logged publicly at /api/security?view=honeypot.

Layer 7: Threat Intelligence (3 feeds)

Real-time IOC feeds from abuse.ch:

  • URLhaus — last 1000 malicious URLs (5-min cache)
  • MalwareBazaar — last 100 malware sample hashes
  • ThreatFox — IOCs from active malware campaigns (7-day window)

Used to check skill source URLs and file hashes. If a skill's source URL is in URLhaus, it gets quarantined.

Layer 8: Auto-Quarantine

If any layer flags a skill as critical/high:

  1. Skill certificate moves to _data/quarantine/
  2. Skill removed from public catalog (skills_index.json)
  3. Listed publicly at /api/security?view=quarantine for transparency
  4. Pre-import scan in the auto-discovery pipeline blocks it before entering the catalog

What actually happened when I ran all 8 layers

Re-audited all 14,581 skills with the new layers.

Result: 0 skills in quarantine.

The catalog was clean — the only malicious skill (prospector-email-finder) had already been removed manually. The 8 layers now run on every new skill import and every weekly batch re-audit.

The stack

  • Vercel Hobby (free tier, 11 serverless functions)
  • GitHub Actions for batch audits (120-min timeout, 16GB RAM)
  • Docker + gVisor for L2 sandbox (--network none, --read-only, --cap-drop ALL)
  • Base (L2) for USDC payment verification
  • ethers.js for EIP-191 signature verification
  • abuse.ch for threat intel feeds (free, no API key)

Total infrastructure cost: $0/month. The marketplace is free. The security infrastructure is the product.

Try it

Not selling anything. Looking for feedback from people who run MCP servers in production — what would make you trust a marketplace enough to install skills from it?

Edison Flores, AliceLabs LLC

Top comments (7)

Collapse
 
nazar-boyko profile image
Nazar Boyko

Naming the specific failure class per layer is what makes this readable instead of one big "we scan for malware" claim, so that part really works. The honest tension I'd sit with is that layers 3 and 4 are pattern and family matching, which is exactly the shape of defense that missed the first trojan until you'd already seen it. The next one won't match a known family or the prospector signature, so those layers start clean-again by definition. That's not a knock, signatures catch the cheap 90%, but it does mean the "0 in quarantine" result is partly "nothing matched what we know," not "nothing bad is present." Do any of the layers flag on behavior or capability rather than known-bad strings, so an unknown payload still trips something? That's the gap I'd worry about with untrusted skills.

Collapse
 
wrencalloway profile image
Wren Calloway

The thing that jumps out: layers 1-8 all inspect the artifact at import time, but MCP skills are live code that talks to servers you don't control. The trojan-in-a-zip is the easy threat because it's static — you can scan it once and be done. The hard one is a skill that ships clean and then pulls its payload at runtime, or a server that returns benign tool descriptions to your scanner and malicious ones to a real client. Your L1.7 flag on raw.githubusercontent.com/.../.zip in the README is the only layer even looking at that, and a runtime fetch doesn't have to live in the README.

Concretely: your gVisor sandbox is --network none, which means whatever you scan in the sandbox can't demonstrate its network behavior — so the exact class of attack that would evade static scanning is also invisible to your dynamic layer. That's not a hole you can patch with more YARA families; it's the reason "0 in quarantine after re-auditing 14,581" reads as "0 skills tripped my static rules," not "0 malicious skills." The honest version of the trust page would say which threat model each number covers.

Genuinely curious how you'd catch a tool-description-poisoning attack where the MCP server serves different descriptions per client — that's the one I'd want answered before installing from any marketplace, and none of the 8 seem aimed at it.

Collapse
 
alexshev profile image
Alex Shev

The useful part of layered security is knowing what each layer is actually responsible for. A marketplace has too many trust surfaces for one big check to mean much: package identity, permissions, runtime behavior, update path, and user intent all need different evidence.

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

The useful thing here is that you separated the layers by what they can actually see.

That distinction matters a lot for MCP marketplaces because package safety and runtime safety are different problems.

The package scanner can catch binaries, secrets, nested archives, suspicious install scripts, and known vulnerable deps. But the runtime layer still needs to answer questions like:

  • which tools are exposed after install?
  • did the tool catalog change after approval?
  • what scopes did the server request?
  • can a tool create external side effects?
  • are tool calls rate-limited per user/workspace?
  • is there an audit receipt for each invocation?

For database-facing MCP servers, I would treat the exposed tool catalog almost like an API surface: reviewable, versioned, and boring to diff.

Related angle: https://conexor.io/blog/mcp-tool-descriptions-security-boundary?utm_source=devto&utm_medium=comment&utm_campaign=engagement

Static scanning catches the supply-chain blast radius. Runtime policy catches the agent blast radius. You need both.

Collapse
 
neelagiri65 profile image
Neelagiri65

eight layers is a lot to maintain honestly.. the useful bit here is which ones actually caught something in practice versus which ones exist because a checklist said so. did you track that per layer ? or is it still a hunch which ones are pulling their weight?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.