Back to skill

Security audit

mcp-sanctions-check

Security checks for vulnerabilities and agentic risk

Overview

This sanctions-checking skill appears purpose-aligned, but its unpinned npm execution, package identity mismatch, and weak cache/data-source handling create review-worthy supply-chain and compliance-integrity risks.

Install only after reviewing the package source and preferably pinning the exact npm version or running a locked local install. Treat results as a screening aid rather than a final compliance decision, and be aware that the tool caches downloaded sanctions data in a predictable temp file and depends on unauthenticated CSV download integrity.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T08 · Insecure Dependencies

Error
Location
SKILL.md:19
Finding
Unpinned npm Package Is Downloaded and Executed Without Confirmation<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:19-22` **Vulnerability Type**: Unpinned dependency execution and supply-chain exposure **Risk Level**: High ### Vulnerable Code ```json { "mcpServers": { "sanctions-check": { "command": "npx", "args": ["-y", "@vbotholemu/mcp-sanctions-check"] } } } ``` The project also declares dependencies using non-exact version ranges and does not include a lockfile: ```json "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", "csv-parse": "^5.6.0", "zod": "^3.23.0" }, "devDependencies": { "typescript": "^5.7.0", "@types/node": "^22.0.0" } ``` ### Technical Analysis The documented configuration invokes `npx` with `-y` and no package version. This causes npm to retrieve and execute the registry's currently resolved release without confirmation. Consequently, the code executed on a user's system is not necessarily the source reviewed in this audit. The absence of a package lockfile and the use of caret dependency ranges also allow dependency resolution to change over time. Although semantic-version constraints limit the accepted versions, they do not guarantee that future compatible releases contain the same audited code. There is also an identity inconsistency: `README.md` refers to `@velocibot/mcp-sanctions-check`, while `SKILL.md` and `package.json` identify `@vbotholemu/mcp-sanctions-check`. This inconsistency increases package-confusion and operator-error risks. ### Attack Path 1. An attacker compromises the package publisher account, npm package, or a transitive dependency. 2. The attacker publishes a malicious version that satisfies the unpinned package request or declared dependency ranges. 3. A user follows the documented configuration and starts the MCP server. 4. `npx -y` downloads the newly resolved package without asking for confirmation. 5. npm or the MCP host executes the malicious package with the privileges of the invoking user. ### Impact Assessmen ...[truncated 469 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin the package to an exact reviewed version, for example: ```json "args": ["-y", "@vbotholemu/mcp-sanctions-check@1.0.0"] ``` - Prefer installing from a lockfile with registry integrity hashes and running the verified local installation instead of downloading code at every launch. - Remove `-y` where interactive confirmation is appropriate. - Commit a package lockfile and use deterministic installation commands such as `npm ci`. - Consider pinning direct dependencies to exact versions and use automated dependency review before updates. - Correct `README.md`, `SKILL.md`, and `package.json` so that they consistently identify one verified npm package and publisher. - Verify package signatures, provenance attestations, and registry ownership as part of release and deployment procedures. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/index.ts:13
Finding
Predictable Shared Temporary Cache Allows Symlink Clobbering and Sanctions-Data Poisoning<![CDATA[ ## Vulnerability Details **File Location**: `src/index.ts:13-67` **Vulnerability Type**: Unsafe temporary-file handling **Risk Level**: Medium ### Vulnerable Code ```ts const CACHE_FILE = path.join(os.tmpdir(), "ofac-sdn-cache.csv"); const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours interface SDNEntry { ent_num: string; name: string; type: string; programs: string; title: string; remarks: string; } function downloadFile(url: string, dest: string): Promise<void> { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); https.get(url, (response) => { if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (redirectUrl) { file.close(); fs.unlinkSync(dest); downloadFile(redirectUrl, dest).then(resolve, reject); return; } } if (response.statusCode && response.statusCode !== 200) { file.close(); fs.unlink(dest, () => {}); reject(new Error(`HTTP request failed with status code ${response.statusCode}`)); return; } response.pipe(file); file.on("finish", () => { file.close(); resolve(); }); }).on("error", (err) => { fs.unlink(dest, () => {}); reject(err); }); }); } async function getSDNData(): Promise<SDNEntry[]> { let needsDownload = true; if (fs.existsSync(CACHE_FILE)) { const stats = fs.statSync(CACHE_FILE); const age = Date.now() - stats.mtimeMs; if (age < CACHE_MAX_AGE_MS) { needsDownload = false; } } if (needsDownload) { await downloadFile(SDN_URL, CACHE_FILE); } const csvContent = fs.readFileSync(CACHE_FILE, "utf-8"); ``` ### Technical Analysis The application uses a constant filename under the operating system's shared temporary directory. It opens that path through `fs.createWriteStream()` without establishing a privat ...[truncated 2038 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create a per-user application cache directory with permissions restricted to the current user, such as mode `0700`. - Create temporary download files with unpredictable names and restrictive permissions. - Use exclusive creation and no-follow semantics where supported; reject symbolic links explicitly. - Validate that an existing cache is a regular file, is owned by the expected user, and has safe permissions. - Download into a securely created temporary file in the same private directory, validate it, and atomically rename it over the cache. - Avoid separate check-then-use operations where possible. - Validate the CSV structure and expected record characteristics before accepting it. - Consider cryptographic authenticity verification if the data publisher provides signatures or trusted checksums. - Coordinate concurrent readers and writers through locking or atomic replacement. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/index.ts:12
Finding
Unrestricted Data-Source Override and Redirect Handling Permit Sanctions-Feed Substitution<![CDATA[ ## Vulnerability Details **File Location**: `src/index.ts:12-44` **Vulnerability Type**: Unvalidated external data source and unrestricted redirects **Risk Level**: Medium ### Vulnerable Code ```ts const SDN_URL = process.env.SDN_URL || "https://www.treasury.gov/ofac/downloads/sdn.csv"; const CACHE_FILE = path.join(os.tmpdir(), "ofac-sdn-cache.csv"); const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours interface SDNEntry { ent_num: string; name: string; type: string; programs: string; title: string; remarks: string; } function downloadFile(url: string, dest: string): Promise<void> { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); https.get(url, (response) => { if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (redirectUrl) { file.close(); fs.unlinkSync(dest); downloadFile(redirectUrl, dest).then(resolve, reject); return; } } if (response.statusCode && response.statusCode !== 200) { file.close(); fs.unlink(dest, () => {}); reject(new Error(`HTTP request failed with status code ${response.statusCode}`)); return; } response.pipe(file); file.on("finish", () => { file.close(); resolve(); }); }).on("error", (err) => { fs.unlink(dest, () => {}); reject(err); }); }); } ``` ### Technical Analysis The source URL can be replaced through the undocumented `SDN_URL` environment variable. The implementation does not restrict the hostname to an official Treasury domain or otherwise authenticate the contents beyond the HTTPS transport connection. HTTP 301 and 302 responses are recursively followed without: - A destination hostname allowlist - A same-origin restriction - A maximum redirect count - A response timeout - A maximum response size - Validation that the ...[truncated 1877 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the production `SDN_URL` override unless it is operationally required. - If configurability is required, validate the URL against an explicit allowlist of official HTTPS hosts. - Reject cross-origin redirects, or independently validate every redirect destination against the same allowlist. - Enforce a small maximum redirect count and detect redirect loops. - Configure connection and response timeouts. - Enforce a maximum response size before and during streaming. - Download to a temporary file and reject the file unless its format, columns, encoding, and expected dataset characteristics pass validation. - Verify a publisher-provided signature or trusted digest when available. - Record the final source URL, retrieval time, and validation result for compliance auditing. - Fail closed when feed authenticity or freshness cannot be established rather than silently using untrusted data. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (15)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared description promises a concrete compliance-screening capability with specific data access and matching behavior. However, the provided code chunk is just an empty declaration file and does not implement any of those functions. This is a material mismatch because the actual supplied code does not support the stated primary purpose at all.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
The skill executes an external package via `npx -y @vbotholemu/mcp-sanctions-check` without pinning an exact version. This creates a supply-chain risk: future package updates or a compromised publisher account could cause different code to run than what was reviewed, potentially leading to arbitrary code execution or data exfiltration.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill solicits sanctions screening of names for compliance use but does not clearly disclose that submitted names are sent to an external MCP service and that OFAC data is downloaded and cached locally. In compliance and KYC contexts, names and related identifiers can be sensitive personal or business data, so hidden transmission and local retention increase privacy, confidentiality, and data-handling risk.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The skill claims to check names against the official OFAC SDN list, but `SDN_URL` can be overridden by an environment variable, allowing the tool to silently use an arbitrary remote CSV instead of the authoritative source. In a compliance-screening context, this can cause false negatives or manipulated results that let sanctioned parties pass screening, which is a security and regulatory integrity issue even if no code execution occurs.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
This code automatically downloads the OFAC SDN CSV over HTTPS and writes it to a temporary local cache file, but there is no user-facing prompt, log message, comment, or other disclosure around that behavior. Because this is a code file, outbound network access and file writes should have some visible warning unless clearly disclosed elsewhere in the skill description, which is not present in this file.

Missing User Warnings

Low
Confidence
88% confidence
Finding
This code automatically downloads the OFAC SDN dataset over HTTPS and writes it to a temporary local cache file before processing it. There is no confirmation prompt, user-facing log, or inline disclosure near this behavior, so users may not realize the tool performs network access and local file writes when invoked.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"author": "velocibot",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1",
    "csv-parse": "^5.6.0",
    "zod": "^3.23.0"
  },
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unverifiable Dependency: @modelcontextprotocol/sdk has 3 known advisory(ies) (CVE-2026-25536 (@modelcontextprotocol/sdk has cross-client data leak via shared server/transport); CVE-2026-0621 (Anthropic's MCP TypeScript SDK has a ReDoS vulnerability); CVE-2025-66414 (Model Context Protocol (MCP) TypeScript SDK does not enable DNS rebinding protec)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
84% confidence
Finding
The manifest allows a floating @modelcontextprotocol/sdk version while known advisories exist for some releases, so consumers may install an affected version without realizing it. In an MCP skill that processes compliance queries and likely exposes network-facing tool behavior, SDK flaws such as data leakage, ReDoS, or missing DNS rebinding protections can meaningfully expand attack surface.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1",
    "csv-parse": "^5.6.0",
    "zod": "^3.23.0"
  },
  "devDependencies": {
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unverifiable Dependency: csv-parse has 2 known advisory(ies) (CVE-2019-17592 (Regular Expression Denial of Service in csv-parse); CVE-2026-85063 (node-csv: Prototype replacement still reachable via columns path)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
78% confidence
Finding
csv-parse has known advisories in some versions, and the unpinned manifest makes it impossible to verify whether installations will avoid affected releases. Because this skill downloads and parses external OFAC CSV data, parser-level DoS or unsafe object-handling bugs could be triggered through maliciously crafted or tampered input if transport or source integrity is compromised.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1",
    "csv-parse": "^5.6.0",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "typescript": "^5.7.0",
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unverifiable Dependency: zod has 1 known advisory(ies) (CVE-2023-4316 (Zod denial of service vulnerability)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
74% confidence
Finding
The zod dependency is not pinned despite a known advisory affecting some versions, so the installed package may be vulnerable depending on resolution time. If the skill validates untrusted MCP inputs with an affected release, crafted payloads could cause denial-of-service through expensive validation paths.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"zod": "^3.23.0"
  },
  "devDependencies": {
    "typescript": "^5.7.0",
    "@types/node": "^22.0.0"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "devDependencies": {
    "typescript": "^5.7.0",
    "@types/node": "^22.0.0"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Static analysis

No suspicious patterns detected.