DEV Community

Edison Flores
Edison Flores

Posted on

I caught a trojan in my MCP marketplace. Here's the 8-layer defense I built.

Two weeks ago, a Windows trojan slipped into my MCP (Model Context Protocol) marketplace. The malware was Trojan:Win64/Lazy.PGPK!MTB, hidden inside a nested zip in a skill package.

This is the technical writeup of what happened, what I built to prevent it, and the architecture of the 8-layer defense pipeline now running in production.

The attack

Vector: Typosquatting GitHub repository

A legitimate MCP server prospector-mcp-email-finder existed. An attacker created a typosquatting copy at JuanquiFortuny/prospector-mcp-email-finder (now taken down) with an identical README — but with a "Download Latest Release" badge linking to a malicious zip on raw.githubusercontent.com.

The payload:

Application.cmd  →  'start unit.exe package.txt'
package.txt      →  298 KB of obfuscated Lua bytecode
unit.exe         →  872 KB PE32+ Windows executable
Enter fullscreen mode Exit fullscreen mode

Windows Defender flagged unit.exe as Trojan:Win64/Lazy.PGPK!MTB. The Application.cmd → unit.exe + package.txt pattern is a classic staged launcher — small CMD wrapper executes the binary with the bytecode payload as input.

How it got in: My import script (which fetches MCP servers from GitHub) downloaded the zip and committed it to dist/skills/ without scanning inside it. My existing audit pipeline (Sentinel L1.5 + L1.6) only checked skill metadata — name, description, system_prompt. It never looked inside the actual package.

The defense — 8 layers

L1.5 — Metadata checks (6 rules)

Cheapest layer. Runs on every skill's metadata:

  • AUTH: Is auth required? (warning if not)
  • TOOL_DESCRIPTIONS: Prompt injection patterns (ignore previous instructions)
  • INPUT_VALIDATION: File/SQL/HTTP access declared in metadata
  • CORS_ORIGIN: Access-Control-Allow-Origin: * (warning)
  • OAUTH_SCOPES: Unscoped tokens
  • RATE_LIMITING: No rate limit declared

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

18 Semgrep-equivalent rules implemented as JS regex (no Semgrep binary needed):

  • Prompt injection (6 patterns)
  • Command injection (4 patterns)
  • SSRF (2 patterns)
  • Path traversal (2 patterns)
  • Tool name spoofing (1 pattern)
  • Hardcoded secrets (3 patterns)

18 secret detection patterns:

  • Stripe (sk_live_*, sk_test_*)
  • GitHub (ghp_*, gho_*, ghs_*)
  • AWS (AKIA*, aws_secret_access_key)
  • Private keys (RSA/EC/OPENSSH)
  • Wallet mnemonics
  • JWT, Slack, Discord, Google, Twilio tokens

OSV API for known vulnerable dependencies.

Bug fix (from peer review): process.env.X references were triggering MCP-SL-001 (hardcoded API key). Fixed by stripping process.env.* patterns before running secret detection.

L1.7 — Binary & malware detection (8 patterns) — NEW

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

const BINARY_EXTENSIONS = ['.exe', '.dll', '.scr', '.msi'];
const LAUNCHER_EXTENSIONS = ['.bat', '.cmd', '.vbs', '.ps1'];
Enter fullscreen mode Exit fullscreen mode

Plus 8 regex patterns for:

  • Staged launchers: start X.exe Y.txt (the exact prospector signature)
  • Obfuscated Lua bytecode: function(o,R,F,U,b,p,E,M,Z,W,...) (high-arity function signature)
  • External download URLs: raw.githubusercontent.com/.../...zip
  • "Download Latest Release" badges with external zip links
  • PowerShell -encodedcommand with long base64
  • eval(atob(...)) obfuscation
  • Lua numeric comment markers
  • Oversized text files >100KB that aren't valid JSON (bytecode payloads)

Quarantine flow: Any critical finding → score 0, risk_level critical, status quarantined, removed from catalog, listed at /api/security?view=quarantine.

L1.8 — Malware family signatures (17 families) — NEW

YARA-equivalent rules for specific malware families, each with MITRE ATT&CK technique IDs:

Family MITRE What it does
Win64/Lazy.PGPK T1027.002 The trojan that hit us
Emotet T1027.011 Banking trojan, Office macro delivery
Cobalt Strike T1071.001 Post-exploitation beacon
Mimikatz T1003.001 LSASS credential dumper
QakBot T1027 Banking trojan
TrickBot T1027 Modular trojan, ransomware precursor
Agent Tesla T1056.001 Keylogger
RedLine Stealer T1555 Browser credential theft
Vidar Stealer T1555 Browser + crypto wallet theft
Raccoon Stealer T1555 MaaS stealer
LummaC2 Stealer T1555 Telegram-sold stealer
AsyncRAT T1071.001 Open-source RAT
njRAT T1071.001 .NET RAT
Remcos RAT T1071.001 Commercial RAT
SolarMarker T1027 SEO-poisoned backdoor
Lokibot T1555 Credential stealer
DoS tools T1499 hping3, slowloris, goldeneye

Any match → instant quarantine.

WAF — Web Application Firewall (40 rules) — NEW

Inspects every incoming HTTP request:

SQLi (7): UNION SELECT, OR 1=1, stacked, time-based, info_schema, hex
XSS (7): script, event handlers, javascript:, data:, vbscript:, img/svg
Path traversal (5): ../, encoded, /etc/passwd, /proc/self, Windows
SSRF (6): AWS/GCP/Azure metadata, file://, gopher://, dict://
Command injection (5): backticks, $(), chained, pipe, && ||
NoSQL (3): $where, $ne, $gt
Prototype pollution: __proto__, constructor.prototype
SSTI: Jinja2 {{ }}, Twig {% %}, JS ${ }
Log injection: \n \r header injection
Enter fullscreen mode Exit fullscreen mode

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

Honeypot (50+ paths) — NEW

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

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

Each hit: IP banned 24h + logged publicly at /api/security?view=honeypot.

Threat Intelligence (3 feeds) — NEW

Real-time IOC feeds from abuse.ch (free, no API key):

  • 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.

Auto-Quarantine

If any layer flags a skill as critical/high:

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

Result

Re-audited all 7,063 skills with the 8 layers. 0 in quarantine. The catalog was clean except for the one we already removed.

Architecture

Request → Honeypot check → WAF (40 rules) → Handler
                                          ↓
Skill import → L1.5 (6) → L1.6 (36) → L1.7 (8) → L1.8 (17) → Quarantine?
                                                                ↓ no
                                                          Catalog (public)
                                                                ↓ yes
                                                          _data/quarantine/
                                                          (publicly listed)
Enter fullscreen mode Exit fullscreen mode

Stack: Vercel Hobby ($0/mo), GitHub Actions (free), Docker + gVisor for L2 sandbox, abuse.ch feeds.

Lessons

  1. Metadata audits are not enough. Open the packages.
  2. Typosquatting is the #1 threat. "Download Latest Release" badges pointing to external zips.
  3. Transparency builds trust. Publish the quarantine list, the audit methodology, the incident history.
  4. Free tier is sufficient. $0/month infrastructure, 8 layers, real cryptography.

Links

Edison Flores, AliceLabs LLC

Top comments (0)