SkillLeak, Browser Credential Theft via MCP Skill

SkillLeak: A Browser-Credential Decryptor Delivered Through an MCP Skill

TL;DR

@szc-ft/mcp-szcd-client is a Model Context Protocol (MCP) client published to npm that auto-configures AI coding assistants — the kind of glue package a developer installs so an IDE agent can talk to a component-library service. It has a long, ordinary-looking release history (71 versions) and a benign install step. Nothing in its postinstall touches credentials.

The credential-reading behavior lives one layer deeper, in a bundled MCP skill called local-browser-test. When that skill's connect() routine runs, it invokes _decryptCredentials() — which decrypts every saved password in the local Chrome/Edge profile (all sites, not the single application a browser test would target) using the Windows Data Protection API (DPAPI) and AES-256-GCM. The decrypted set is written in plaintext to ~/.szcd-mcp/deps/decrypted-creds.json and returned inside the skill's MCP tool result, which flows to the package's default remote MCP server, mcp.szcd-mcp.top.

This is a supply-chain pattern worth naming: the sensitive capability is not in an install hook (where most scanners and reviewers look) but in an MCP skill that only executes when an operator points the tool at their own running browser. We are calling it SkillLeak. MEW classifies @szc-ft/mcp-szcd-client (versions 0.38.0 and 0.39.0) as malicious.

Term note — MCP skill: a self-contained bundle of instructions and helper scripts that an MCP server exposes to an AI agent as a callable capability. The agent invokes it like a tool; the scripts run on the developer's machine.

Attack Anatomy

The package presents two very different faces.

The face a reviewer sees first is the install path. The manifest declares postinstall: node scripts/postinstall.js, and that script does exactly what the README says: it wires MCP integrations into IDEs and installs a sibling package, @szc-ft/sketch-mcp-server. It never reaches the credential code. A scanner that stops at install hooks — the historically highest-signal place to look — sees a clean, unremarkable developer tool. This package had in fact been assessed as safe in an earlier review pass.

The face that matters is reached only through the bundled skill. The chain:

  • A developer runs the local-browser-test skill against a Chrome/Edge instance they started with a remote-debugging port. This is the documented way to use the skill.
  • The skill’s connect() (lib/browser-engine.js) calls _decryptCredentials() at line 246.
  • _decryptCredentials() calls decryptAllPasswords({ filter: ‘%’ }). The ‘%’ filter is the detail that turns a test convenience into a harvest: it matches every stored origin rather than the single site under test.
  • lib/decrypt-passwords.js reads the browser’s os_crypt encrypted key, runs powershell … ProtectedData::Unprotect (DPAPI) to recover the AES key, then AES-256-GCM-decrypts each saved credential.
  • The full decrypted set is written to ~/.szcd-mcp/deps/decrypted-creds.json in cleartext and returned as the skill’s MCP tool result.
  • That result travels back over the MCP channel to the package’s default server, mcp.szcd-mcp.top — the same host documented in the README and in scripts/lib/common.js.

A brief note on why step 4 works at all. On Windows, Chrome and Edge do not store saved passwords in plaintext: each credential is encrypted with an AES-256-GCM key, and that key is itself sealed with DPAPI, which binds it to the current user account. Recovering a password therefore takes two moves — first ProtectedData::Unprotect to recover the AES key (which succeeds because the code runs as the logged-in user, exactly the context a developer’s terminal provides), then a GCM decrypt of each stored blob read from the browser’s os_crypt-protected store. This is well-understood, offline, and silent: no master-password prompt, no browser interaction, no network round-trip until the results are forwarded. It is the same primitive many commodity infostealers use — the novelty here is only the wrapper that carries it.

The decryptor is not incidental. It is bundled three times — under standard-skill/, opencode-extension/, and qwen-extension/ — one copy per supported AI-assistant surface, so the capability travels with whichever assistant the developer wired up.

The contrast with a legitimate browser-test auto-login is the whole story. A genuine test helper decrypts the credential for the one application it is exercising, uses it in-process, and never persists or forwards it. Here the routine decrypts everything, writes it to disk in the clear, and returns it across a network boundary to a remote server.

Timeline & Trigger 

Two behaviors matter for understanding exposure.

The trigger is opt-in, not automatic. Installing the package does not read any credentials. The decryption runs only when a developer actively invokes the local-browser-test skill against a browser they have launched in remote-debugging mode. That materially narrows who is affected compared with a postinstall payload that fires on every npm install.

The packaging is mature, not throwaway. The publisher has shipped 71 versions and maintains a broader @szc-ft component-library MCP suite. The credential decryptor is present in both flagged versions (0.38.0 and 0.39.0) with an identical fingerprint, invoked from the same browser-engine.js:246 entry point. The package remains live on npm at the time of writing.

That combination — an established publisher, a benign install step, and a capability that only fires under a specific opt-in workflow — is exactly why an automated classifier predicted “safe” and an earlier review agreed. The behavior only resolves under manual reading of the skill’s helper scripts.

Indicators of Compromise

Type Indicator
Package @szc-ft/mcp-szcd-client (npm) — versions 0.38.0, 0.39.0
Network Default remote MCP server mcp.szcd-mcp.top (README.md, scripts/lib/common.js:18)
File (dropped) ~/.szcd-mcp/deps/decrypted-creds.json — plaintext decrypted credentials
File (payload) .../local-browser-test/lib/decrypt-passwords.js — DPAPI ProtectedData::Unprotect + AES-256-GCM (bundled under standard-skill/, opencode-extension/, qwen-extension/)
Behavioral browser-engine.js:246 _decryptCredentials()decryptAllPasswords({ filter: '%' }) (all-site scope)
Behavioral Reads Chrome/Edge os_crypt key; requires a browser started with --remote-debugging-port
Install hook postinstall: node scripts/postinstall.jsbenign (IDE MCP config + sibling-package install); does not reach the decryptor

Defenders auditing MCP tooling should treat the skill directory as in-scope, not just the manifest and install scripts.

Attribution & observed behavior

We describe behavior, not motive.

The package is published under an established npm account with a coherent multi-package @szc-ft MCP suite and a long version history. There is no dedicated covert command-and-control channel: the decrypted credentials are returned through the package’s own documented MCP server, mcp.szcd-mcp.top, rather than to an anonymous drop point. The install step is genuinely benign.

What moves this from “aggressive convenience feature” to a malicious classification is the observable scope-and-destination combination: the routine decrypts all saved browser credentials rather than the single application a test would exercise, persists them to disk in plaintext, and transmits them off the host across the MCP channel. A browser-test auto-login feature that behaved this way would be indistinguishable, in effect, from credential harvesting — and the effect is what a defender must reason about.

We note honestly that the trigger is opt-in and the publisher is established; both facts are in the record above. Neither changes the observable outcome for a developer who runs the skill: the saved passwords for every site in their browser profile leave the machine.

SkillLeak is a single-package incident, but the delivery surface it uses is the point.

The industry’s supply-chain reflexes are tuned to install-time execution: preinstall/postinstall hooks, dependency-confusion beacons, typosquats with a payload in index.js. MCP changes the geometry. An MCP package can ship a manifest and install step that are entirely clean while carrying its consequential behavior inside a skill — a bundle the AI agent invokes later, on the developer’s own instruction, against the developer’s own resources. The capability rides in with the assistant integration and lies dormant until a normal-looking workflow (“run the browser test”) activates it.

For teams adopting MCP tooling:

  • Audit skills, not just manifests. Extend package review to the skills/, *-extension/, and equivalent directories. The most sensitive code may never appear in an install hook.
  • Watch for scope inflation. A capability that claims a narrow purpose (test one app) but operates broadly (filter: ‘%’ over all stored origins) is a red flag independent of intent.
  • Treat MCP tool results as an egress channel. Data returned from a skill leaves the host to whatever MCP server the client is configured for. Inventory those destinations the way you would any outbound connection.
  • Re-review version bumps of “trusted” MCP packages. An established publisher and a long release history are not a substitute for reading the code that ships in each version — as this case, which reverses an earlier safe assessment, demonstrates.

Concretely, three low-cost checks would have surfaced this package before a developer ran the skill. First, a content scan of skill directories for local credential-store access — references to os_crypt, ProtectedData/DPAPI, or a browser’s Login Data file — flags the capability regardless of which install hook exists. Second, a scope check on any decrypt/read routine that accepts a wildcard selector (filter: ‘%’ and equivalents) rather than a bound identifier catches the test-to-harvest inflation. Third, diffing the set of hosts a package can reach (here, mcp.szcd-mcp.top) against the destinations its documentation claims to need turns the MCP result channel into an auditable egress point. None of these require running the code.

Credential decryption via DPAPI and os_crypt is old technique; delivering it through an AI-agent skill that only fires under an opt-in workflow is the new wrapper. Expect more of the capability-in-a-skill pattern as MCP tooling proliferates, and adjust review scope accordingly. 

sca-tools-software-composition-analysis-tools
Prioritize, remediate, and secure your software risks
Get your Free Account.
No credit card required.

Secure your Software Development and Delivery

with Xygeni Product Suite