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_*, GitHubghp_*, AWSAKIA*) - 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_fileto 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.txtpattern — 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
-encodedcommandwith 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:
- Skill certificate moves to
_data/quarantine/ - Skill removed from public catalog (
skills_index.json) - Listed publicly at
/api/security?view=quarantinefor transparency - 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
- Browse: https://marketnow.site
- Install:
npx -y marketnow-mcp - Security overview: https://marketnow.site/api/security
- Trust page: https://marketnow.site/trust
- GitHub: https://github.com/edgarfloresguerra2011-a11y/marketnow
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)
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.
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/.../.zipin the README is the only layer even looking at that, and a runtimefetchdoesn'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.
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.
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:
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.
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.