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

> Source: <https://dev.to/edison_flores_6d2cd381b13/i-caught-a-trojan-in-my-mcp-marketplace-heres-the-8-layer-defense-i-built-gk4>
> Published: 2026-07-18 03:27:03+00:00

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.

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

Cheapest layer. Runs on every skill's metadata:

`ignore previous instructions`

)`Access-Control-Allow-Origin: *`

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

18 secret detection patterns:

`sk_live_*`

, `sk_test_*`

)`ghp_*`

, `gho_*`

, `ghs_*`

)`AKIA*`

, `aws_secret_access_key`

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

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

``` js
const BINARY_EXTENSIONS = ['.exe', '.dll', '.scr', '.msi'];
const LAUNCHER_EXTENSIONS = ['.bat', '.cmd', '.vbs', '.ps1'];
```

Plus 8 regex patterns for:

`start X.exe Y.txt`

(the exact prospector signature)`function(o,R,F,U,b,p,E,M,Z,W,...)`

(high-arity function signature)`raw.githubusercontent.com/.../...zip`

`-encodedcommand`

with long base64`eval(atob(...))`

obfuscation**Quarantine flow:** Any critical finding → score 0, risk_level `critical`

, status `quarantined`

, removed from catalog, listed at `/api/security?view=quarantine`

.

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.

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

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`

.

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

Used to check skill source URLs and file hashes.

If any layer flags a skill as critical/high:

`_data/quarantine/`

`/api/security?view=quarantine`

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

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

— *Edison Flores, AliceLabs LLC*
