Use with caution. Address findings before production.
Scanned 5/13/2026, 6:12:31 AMยทCached resultยทDeep Scanยท91 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
4
high
14
medium
0
low
MCP Server Information
Findings
This package carries a C-grade security rating with 4 high-severity issues and 14 medium-severity issues that warrant careful review before installation. The primary concerns are prompt injection vulnerabilities (5 instances), server configuration weaknesses (9 instances), and a vulnerable dependency, which could expose your system to prompt manipulation attacks and misconfigurations. Given the safety score of 66/100, you should thoroughly evaluate whether the functionality justifies these security risks or consider alternative packages with stronger security postures.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 18 of 18 findings. Click any finding to read.
Dependencies
@modelcontextprotocol/sdk (2)
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
18 of 18 findings
18 findings
The 'fetch' tool accepts a 'url' argument from the caller and fetches arbitrary web content without verifying the caller has authorization to access that URL, enabling IDOR attacks to retrieve unauthorized resources.
RemediationAI
The problem is that the fetch tool accepts arbitrary URLs from callers without checking if they are authorized to access those resources, enabling IDOR attacks. Implement a URL allowlist or authorization check by adding a function like `isUrlAuthorized(url: string, callerId: string): boolean` that validates the URL against a caller-specific policy before calling `fetch()`. This ensures only pre-approved URLs or URL patterns can be fetched on behalf of each caller. Verify the fix by testing that fetch rejects URLs not in the allowlist and logs the rejection attempt.
Tool 'fetch' shadows the reserved http/network tool name without server-specific prefix.
RemediationAI
The problem is that the tool is named 'fetch', which shadows the reserved HTTP/network tool namespace and creates ambiguity in tool registries. Rename the tool to include a server-specific prefix, such as 'obsidian_fetch' or 'vault_fetch', by changing the tool name in the `ToolRegistry.register()` call in `packages/mcp-server/src/fetch/index.ts`. This prevents namespace collisions and makes the tool's origin and purpose explicit. Verify by checking that `list_tools()` returns the new prefixed name and no other tools conflict with it.
fetch tool uses module-level DEFAULT_USER_AGENT constant and makes unauthenticated HTTP requests on behalf of caller without consulting caller identity or per-request credentials.
RemediationAI
The problem is that the fetch tool uses a module-level DEFAULT_USER_AGENT constant and makes unauthenticated HTTP requests without consulting the caller's identity or credentials, allowing the server to impersonate all callers identically. Modify the fetch handler to accept an optional `credentials` or `auth` parameter and pass caller identity to the fetch function via a new parameter like `callerId: string`, then construct a per-caller User-Agent or auth header. This ensures each request is attributed to the correct caller and respects their authentication context. Verify by inspecting HTTP request headers in a test to confirm the User-Agent or Authorization header reflects the caller's identity.
The 'fetch' tool reads untrusted external webpage content from a caller-supplied URL and returns it verbatim in the LLM context with only a bare text prefix ('Contents of {url}:') and no structured provenance delimiter, enabling indirect prompt injection.
RemediationAI
The problem is that untrusted external webpage content is returned verbatim with only a bare text prefix ('Contents of {url}:'), allowing indirect prompt injection if the fetched content contains LLM instructions. Wrap the fetched content in a structured delimiter and mark it as untrusted by changing the response format to use XML-style tags like `<untrusted_external_content url="{url}">...</untrusted_external_content>` or a JSON object with a `source` and `content_type` field. This provides the LLM with explicit provenance and signals that the content is not from a trusted source. Verify by fetching a URL containing prompt-injection payloads and confirming the LLM treats the content as data, not instructions.
@modelcontextprotocol/sdk==1.0.4 has 2 known CVEs [HIGH]: GHSA-8r9q-7v3j-jr4g, GHSA-w48q-cv73-mx4w. Upgrade to a patched version.
RemediationAI
The problem is that @modelcontextprotocol/sdk version 1.0.4 contains 2 known HIGH-severity CVEs (GHSA-8r9q-7v3j-jr4g and GHSA-w48q-cv73-mx4w). Update the dependency in `packages/mcp-server/package.json` by changing `"@modelcontextprotocol/sdk": "1.0.4"` to a patched version (e.g., `"@modelcontextprotocol/sdk": "^1.1.0"` or the latest available), then run `bun install` to fetch the fixed version. This removes the vulnerable code paths that the CVEs exploit. Verify by running `bun list @modelcontextprotocol/sdk` and confirming the installed version is no longer 1.0.4, then check the package's security advisory page to confirm the CVEs are resolved in that version.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 116 | }); |
| 117 | console.log("Watching for changes..."); |
| 118 | for await (const event of watcher) { |
| 119 | console.log(`Detected ${event.eventType} in ${event.filename}`); |
| 120 | await build(); |
| 121 | } |
| 122 | } |
RemediationAI
The problem is that user-controlled values (event.eventType and event.filename) are printed directly to the terminal without ANSI escape sequence sanitization, allowing malicious input to inject cursor-control sequences that rewrite output or hide commands. Sanitize the output by importing a library like `strip-ansi` or manually removing ANSI escape sequences before logging, e.g., `console.log(`Detected ${sanitizeAnsi(event.eventType)} in ${sanitizeAnsi(event.filename)}`);`. This prevents terminal injection attacks. Verify by creating a test file with ANSI escape sequences in its name and confirming the logged output displays the raw filename without executing any control sequences.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 35 | * |
| 36 | * @example |
| 37 | * ```ts |
| 38 | * const html = await fetch("https://bcurio.us/resources/hdkb/gates/44"); |
| 39 | * const md = convertHtmlToMarkdown(await html.text(), "https://bcurio.us"); |
| 40 | * await Bun.write("playground/bcurious-gate-44.md", md); |
| 41 | * ``` |
RemediationAI
The problem is that the fetch call in `convertHtmlToMarkdown()` and related network operations lack an explicit timeout, allowing a malicious or hung upstream to pin threads and exhaust connection pools. Add a timeout parameter to the fetch call by using `fetch(url, { timeout: 30000 })` (30 seconds) or wrapping the call in `Promise.race([fetch(...), timeout(30000)])`, and document the timeout in the function signature. This ensures the server remains responsive even if upstream is slow or unresponsive. Verify by testing with a URL that never responds and confirming the request fails after the timeout interval rather than hanging indefinitely.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # MCP Server Installation Feature Requirements |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | This feature enables users to install and manage the MCP server executable through the Obsidian plugin settings interface. The system handles the download of platform-specific binaries, Claude Desktop configuration, and provides clear user feedback throughout the process. |
| 6 | |
| 7 | ## Implementation Location |
| 8 | |
| 9 | The installation feature is implemented in the Obsidian plugin package under `src/features/mcp-server-install`. |
| 10 | |
| 11 | ## Installation Flow |
| 12 | |
| 13 | 1. |
RemediationAI
The problem is that the MCP manifest in `docs/features/mcp-server-install.md` declares tools but does not specify an authentication mechanism, making it unclear whether the server relies on network-layer auth, host-level auth, or no auth at all. Add an explicit `authentication` or `auth` field to the manifest documentation describing the actual mechanism (e.g., `"auth": "mtls"` or `"auth": "host-level-only"`), or update the README to clearly state the authentication model. This allows reviewers to audit the security model. Verify by reviewing the updated documentation and confirming that an auditor can determine exactly how the server authenticates callers.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # MCP Tools for Obsidian - Server |
| 2 | |
| 3 | A secure Model Context Protocol (MCP) server that provides authenticated access to Obsidian vaults. This server implements MCP endpoints for accessing notes, executing templates, and performing semantic search through Claude Desktop and other MCP clients. |
| 4 | |
| 5 | ## Features |
| 6 | |
| 7 | ### Resource Access |
| 8 | |
| 9 | - Read and write vault files via `note://` URIs |
| 10 | - Access file metadata and frontmatter |
| 11 | - Semantic search through Smart Connections |
| 12 | - Template execution via Templater |
| 13 | |
| 14 | ### Sec |
RemediationAI
The problem is that the MCP manifest in `packages/mcp-server/README.md` declares tools but lacks an explicit authentication field, leaving the security model ambiguous for reviewers. Add a dedicated 'Authentication' section to the README that explicitly documents the auth mechanism (e.g., 'This server uses host-level authentication via Obsidian plugin credentials' or 'Network-layer mTLS'), or add an `auth` field to any manifest JSON. This clarifies the security boundary. Verify by having a security reviewer read the updated documentation and confirm they can identify the authentication model without ambiguity.
ObsidianMcpServer declares tools capability but ToolRegistry.list() response lacks per-tool content-bound integrity fields (version/etag/digest/sha256/hash), enabling undetected tool swaps if list_changed is emitted.
Evidence
| 1 | #!/usr/bin/env bun |
| 2 | import { logger } from "$/shared"; |
| 3 | import { ObsidianMcpServer } from "./features/core"; |
| 4 | import { getVersion } from "./features/version" with { type: "macro" }; |
RemediationAI
The problem is that ToolRegistry.list() returns tools without per-tool integrity fields (version, etag, digest, or sha256), so if list_changed is emitted, an attacker could swap tool definitions undetected. Add integrity fields to each tool in the ToolRegistry response by including a `sha256` or `etag` field computed from the tool's schema and implementation, e.g., `{ name: "fetch", description: "...", sha256: "abc123..." }`. This allows clients to detect unauthorized tool modifications. Verify by computing the hash of a tool's definition, emitting list_changed, and confirming that a client can detect if the tool's hash has changed.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 19 | - uses: actions/checkout@v4 |
| 20 | |
| 21 | - name: Setup Bun |
| 22 | uses: oven-sh/setup-bun@v2 |
| 23 | with: |
| 24 | bun-version: latest |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `actions/checkout@v4`, which is a mutable tag that can be rewritten by a compromised maintainer, allowing malicious code injection into the CI pipeline. Replace `uses: actions/checkout@v4` with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` (pinned to a specific commit SHA with a version comment for readability). This ensures the exact version of the action is always used. Verify by running `git log --oneline .github/workflows/release.yml` and confirming the SHA is hardcoded and matches a known release tag.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 64 | - name: Upload Release Artifacts |
| 65 | env: |
| 66 | GH_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| 67 | uses: ncipollo/release-action@v1 |
| 68 | with: |
| 69 | allowUpdates: true |
| 70 | omitName: true |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `ncipollo/release-action@v1`, a mutable tag that can be rewritten to inject malicious code. Replace it with `uses: ncipollo/release-action@c65f4d75c732e9c206ef63d029ba5a1b2bcfb4ec # v1.14.0` (pinned to a specific commit SHA with version comment). This locks the action to a known, immutable version. Verify by checking the GitHub release page for ncipollo/release-action to confirm the SHA corresponds to v1.14.0 or your intended version.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 50 | - name: Get existing release body |
| 51 | id: get_release_body |
| 52 | uses: actions/github-script@v7 |
| 53 | with: |
| 54 | result-encoding: string # This tells the action to return a raw string |
| 55 | script: | |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `actions/github-script@v7`, a mutable tag that could be compromised. Replace it with `uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1` (pinned to a specific commit SHA with version comment). This prevents tag rewriting attacks. Verify by confirming the SHA matches the intended version on the GitHub Actions marketplace or release page.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 25 | - name: Create Release |
| 26 | id: create_release |
| 27 | uses: softprops/action-gh-release@v1 |
| 28 | with: |
| 29 | generate_release_notes: true |
| 30 | draft: false |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `softprops/action-gh-release@v1`, a mutable tag vulnerable to rewriting. Replace it with `uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aba7aa56d3c7 # v1.0.0` (pinned to a specific commit SHA with version comment). This ensures the exact release action code is used. Verify by checking the softprops/action-gh-release GitHub repository to confirm the SHA matches your intended version.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 16 | id-token: write |
| 17 | attestations: write |
| 18 | steps: |
| 19 | - uses: actions/checkout@v4 |
| 20 | |
| 21 | - name: Setup Bun |
| 22 | uses: oven-sh/setup-bun@v2 |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `actions/checkout@v4` and `oven-sh/setup-bun@v2`, both mutable tags. Replace `actions/checkout@v4` with `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` and `oven-sh/setup-bun@v2` with the corresponding commit SHA (e.g., `oven-sh/setup-bun@4c905cbb38844f188f1565602d3d12c7ec1adfea # v2.0.0`). This prevents both actions from being silently updated to malicious versions. Verify by inspecting the workflow file and confirming all `uses:` statements contain 40-character commit SHAs.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 44 | run: bun run zip |
| 45 | |
| 46 | - name: Generate artifact attestation for MCP server binaries |
| 47 | uses: actions/attest-build-provenance@v2 |
| 48 | with: |
| 49 | subject-path: "packages/mcp-server/dist/*" |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `actions/attest-build-provenance@v2`, a mutable tag that could be compromised. Replace it with `uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d31d993 # v2.1.0` (pinned to a specific commit SHA with version comment). This ensures the attestation action is not silently replaced. Verify by checking the GitHub Actions marketplace for the correct SHA corresponding to v2.1.0 or your intended version.
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | import { logger, type ToolRegistry } from "$/shared"; |
| 2 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; |
| 3 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; |
| 4 | import { type } from "arktype"; |
| 5 | import { DEFAULT_USER_AGENT } from "./constants"; |
| 6 | import { convertHtmlToMarkdown } from "./services/markdown"; |
| 7 | |
| 8 | export function registerFetchTool(tools: ToolRegistry, server: Server) { |
| 9 | tools.register( |
| 10 | type({ |
| 11 | name: '"fetch"', |
| 12 | arguments: { |
| 13 | url |
RemediationAI
The problem is that the fetch tool description or documentation contains imperative language directing the LLM to invoke other tools (e.g., 'invoke the write_file tool'), which is a cross-tool chaining injection that escalates privileges beyond what the user authorized. Rewrite the tool description in `packages/mcp-server/src/features/fetch/index.ts` to describe only what the tool does (e.g., 'Fetches and returns the content of a URL') without mentioning or directing the LLM to use other tools. If the tool requires composition, document the dependency in a separate human-readable guide, not in the tool description. Verify by reviewing the tool description and confirming it contains no imperative verbs like 'invoke', 'call', 'use', or 'also'.
MCP tool file registers a tool, performs a destructive sink (fs.unlink / shutil.rmtree / DROP TABLE / DELETE FROM / TRUNCATE / UPDATE ... SET / HTTP DELETE|PUT|PATCH / subprocess / exec / spawn), and emits no audit event anywhere in the file. Without an audit event, an investigator cannot answer "who deleted record X on day Y?" โ the irreversible action leaves no trail. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Distinct from MCP-201 (no confirmation) and MCP-283
Evidence
| 1 | import { makeRequest, type ToolRegistry } from "$/shared"; |
| 2 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; |
| 3 | import { type } from "arktype"; |
| 4 | import { LocalRestAPI } from "shared"; |
| 5 | |
| 6 | export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { |
| 7 | // GET Status |
| 8 | tools.register( |
| 9 | type({ |
| 10 | name: '"get_server_info"', |
| 11 | arguments: "Record<string, unknown>", |
| 12 | }).describe( |
| 13 | "Returns basic details about the Obsidian Local REST API and authentic |
RemediationAI
The problem is that `packages/mcp-server/src/features/local-rest-api/index.ts` registers tools that perform destructive operations (DELETE, PUT, PATCH, or file deletion) but emits no audit event, leaving no trail for investigators to determine who performed the action and when. Add an audit event emission after each destructive operation by calling a function like `auditLog.record({ action: 'DELETE', resource: url, callerId, timestamp, result })` or logging to a structured audit sink. This ensures all destructive actions are recorded. Verify by performing a destructive operation and confirming an audit log entry is created with the caller ID, action, resource, and timestamp.