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
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'];
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
-encodedcommandwith 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
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=quarantinefor 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)
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
Top comments (0)