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

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: Skill certificate moves to _data/quarantine/ Skill removed from public catalog Listed publicly at /api/security?view=quarantine for transparency 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 Metadata audits are not enough. Open the packages. Typosquatting is the #1 threat. "Download Latest Release" badges pointing to external zips. Transparency builds trust. Publish the quarantine list, the audit methodology, the incident history. Free tier is sufficient. $0/month infrastructure, 8 layers, real cryptography. Links Live: https://marketnow.site Security overview: https://marketnow.site/api/security Trust page: https://marketnow.site/trust GitHub: https://github.com/edgarfloresguerra2011-a11y/marketnow — Edison Flores, AliceLabs LLC

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.