CVE-2025-59155
Description
hackmd-mcp is a Model Context Protocol server for integrating HackMD's note-taking platform with AI assistants. From 1.4.0 to before 1.5.0, hackmd-mcp contains a server-side request forgery (SSRF) vulnerability when the server is run in HTTP transport mode. Arbitrary hackmdApiUrl values supplied via the Hackmd-Api-Url HTTP header or a base64-encoded JSON query parameter are accepted without validation, allowing attackers to redirect outbound API requests to internal network services, access internal endpoints, perform network reconnaissance, and bypass network access controls. The stdio transport mode is not affected because it only accepts stdio requests. The issue is fixed in version 1.5.0, which enforces allowed endpoints and supports the ALLOWED_HACKMD_API_URLS environment variable. Users should update to 1.5.0 or later or apply documented mitigations such as switching to stdio mode, restricting outbound network access, or filtering the Hackmd-Api-Url header and related query parameter via a reverse proxy.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
hackmd-mcpnpm | >= 1.4.0, < 1.5.0 | 1.5.0 |
Affected products
1- Range: >= 1.4.0, < 1.5.0
Patches
25 files changed · +10 −10
Dockerfile+1 −1 modified@@ -27,7 +27,7 @@ FROM node:22-alpine AS release LABEL org.opencontainers.image.title="HackMD MCP" LABEL org.opencontainers.image.description="A Model Context Protocol server for integrating HackMD's note-taking platform with AI assistants." -LABEL org.opencontainers.image.version="1.4.2" +LABEL org.opencontainers.image.version="1.5.0" LABEL org.opencontainers.image.vendor="yuna0x0" LABEL org.opencontainers.image.authors="yuna0x0 <yuna@yuna0x0.com>"
index.ts+1 −1 modified@@ -122,7 +122,7 @@ function parseConfig(req: Request): { config?: any; error?: any } { function createServer({ config }: { config: z.infer<typeof ConfigSchema> }) { const server = new McpServer({ name: "hackmd-mcp", - version: "1.4.2", + version: "1.5.0", }); // Initialize HackMD API client with config
manifest.json+1 −1 modified@@ -2,7 +2,7 @@ "manifest_version": "0.1", "name": "hackmd-mcp", "display_name": "HackMD MCP", - "version": "1.4.2", + "version": "1.5.0", "description": "A Model Context Protocol server for integrating HackMD's note-taking platform with AI assistants.", "author": { "name": "yuna0x0",
package.json+1 −1 modified@@ -5,7 +5,7 @@ "module": "index.ts", "type": "module", "license": "MIT", - "version": "1.4.2", + "version": "1.5.0", "author": "yuna0x0 <yuna@yuna0x0.com>", "repository": { "type": "git",
server.json+6 −6 modified@@ -7,13 +7,13 @@ "url": "https://github.com/yuna0x0/hackmd-mcp", "source": "github" }, - "version": "1.4.2", + "version": "1.5.0", "packages": [ { "registry_type": "npm", "registry_base_url": "https://registry.npmjs.org", "identifier": "hackmd-mcp", - "version": "1.4.2", + "version": "1.5.0", "transport": { "type": "stdio" }, @@ -39,7 +39,7 @@ "registry_type": "oci", "registry_base_url": "https://docker.io", "identifier": "yuna0x0/hackmd-mcp", - "version": "1.4.2", + "version": "1.5.0", "transport": { "type": "stdio" }, @@ -63,9 +63,9 @@ }, { "registry_type": "mcpb", - "identifier": "https://github.com/yuna0x0/hackmd-mcp/releases/download/v1.4.2/hackmd-mcp-1.4.2.mcpb", - "file_sha256": "7b6ee105271d8595e3e5a0a3e4f9075ab3a2b7b373f529f4c3e99d1f93dead62", - "version": "1.4.2", + "identifier": "https://github.com/yuna0x0/hackmd-mcp/releases/download/v1.5.0/hackmd-mcp-1.5.0.mcpb", + "file_sha256": "6035e3082ffaf5627e1293a2c8a5d7f42496010431c9b026859dae3bbaa9ce38", + "version": "1.5.0", "transport": { "type": "stdio" },
43936c78a5bbAdd HackMD API URL allowlist to prevent SSRF attacks
3 files changed · +100 −48
env.example+8 −0 modified@@ -6,6 +6,14 @@ HACKMD_API_TOKEN=your_api_token # HackMD API Endpoint URL (defaults to https://api.hackmd.io/v1) # HACKMD_API_URL=https://api.hackmd.io/v1 +## ----------------------------------------------------- +## Optional settings for Streamable HTTP transport mode +## ----------------------------------------------------- + +# Allowed HackMD API URLs (comma-separated list for security) +# If not set, defaults to the official HackMD API URL +# ALLOWED_HACKMD_API_URLS=https://api.hackmd.io/v1,https://your-hackmd-instance.com/api/v1 + # Use TRANSPORT=http for Streamable HTTP transport mode # TRANSPORT=http
index.ts+89 −48 modified@@ -41,13 +41,40 @@ app.use( app.use(express.json()); +// Helper function to create JSON-RPC error responses +function createJsonRpcError(code: number, message: string) { + return { + jsonrpc: "2.0" as const, + error: { code, message }, + id: null, + }; +} + +// Get allowed HackMD API URLs from environment +function getAllowedApiUrls(): string[] { + const allowedUrls = process.env.ALLOWED_HACKMD_API_URLS; + if (!allowedUrls || allowedUrls.trim().length === 0) { + return [DEFAULT_HACKMD_API_URL]; + } + return allowedUrls + .split(",") + .map((url) => url.trim()) + .filter((url) => url.length > 0); +} + +// Validate if the provided API URL is allowed +function isAllowedApiUrl(url: string): boolean { + const allowedUrls = getAllowedApiUrls(); + return allowedUrls.includes(url); +} + // Parse configuration from header or query parameters (for Smithery) -function parseConfig(req: Request) { +function parseConfig(req: Request): { config?: any; error?: any } { const hackmdApiTokenHeader = req.headers[HACKMD_API_TOKEN_HEADER.toLowerCase()]; const hackmdApiUrlHeader = req.headers[HACKMD_API_URL_HEADER.toLowerCase()]; - let config: any = {}; + const config: any = {}; if ( typeof hackmdApiTokenHeader === "string" && @@ -65,20 +92,30 @@ function parseConfig(req: Request) { // If any config found in headers, return it if (Object.keys(config).length > 0) { - return config; + return { config }; } // Smithery passes config as base64-encoded JSON in query parameters const configParam = req.query.config; if (typeof configParam === "string" && configParam.trim().length > 0) { - const smitheryConfig = JSON.parse( - Buffer.from(configParam, "base64").toString(), - ); - return smitheryConfig; + try { + const smitheryConfig = JSON.parse( + Buffer.from(configParam, "base64").toString(), + ); + + return { config: smitheryConfig }; + } catch (error) { + return { + error: createJsonRpcError( + -32000, + "Bad Request: Invalid base64-encoded config parameter", + ), + }; + } } // Return empty config if nothing found - return config; + return { config }; } // Create MCP server with HackMD integration @@ -100,31 +137,53 @@ function createServer({ config }: { config: z.infer<typeof ConfigSchema> }) { // Handle MCP requests at /mcp endpoint app.post("/mcp", async (req: Request, res: Response) => { try { - // Parse configuration - const rawConfig = parseConfig(req); + // Parse configuration with URL validation + const parseResult = parseConfig(req); + + // Check for parsing errors + if (parseResult.error) { + return res.status(400).json(parseResult.error); + } + + const rawConfig = parseResult.config || {}; // Check if API token is available (from header, query param, or env var) const hackmdApiToken = rawConfig.hackmdApiToken || process.env.HACKMD_API_TOKEN; if (!hackmdApiToken || hackmdApiToken.trim().length === 0) { - return res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: `Bad Request: Please provide a HackMD API token via header '${HACKMD_API_TOKEN_HEADER}'.`, - }, - id: null, - }); + return res + .status(400) + .json( + createJsonRpcError( + -32000, + `Bad Request: Please provide a HackMD API token via header '${HACKMD_API_TOKEN_HEADER}'.`, + ), + ); + } + + // Extract API URL from config or use default + const hackmdApiUrl = + rawConfig.hackmdApiUrl || + process.env.HACKMD_API_URL || + DEFAULT_HACKMD_API_URL; + + // Validation of the API URL + if (!isAllowedApiUrl(hackmdApiUrl)) { + return res + .status(400) + .json( + createJsonRpcError( + -32000, + `Bad Request: HackMD API URL "${hackmdApiUrl}" is not in the allowed list`, + ), + ); } - // Validate and parse configuration with fallbacks to environment variables + // Validate and parse configuration const config = ConfigSchema.parse({ hackmdApiToken, - hackmdApiUrl: - rawConfig.hackmdApiUrl || - process.env.HACKMD_API_URL || - DEFAULT_HACKMD_API_URL, + hackmdApiUrl, }); const server = createServer({ config }); @@ -143,41 +202,23 @@ app.post("/mcp", async (req: Request, res: Response) => { } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { code: -32603, message: "Internal server error" }, - id: null, - }); + res.status(500).json(createJsonRpcError(-32603, "Internal server error")); } } }); // SSE notifications not supported in stateless mode app.get("/mcp", async (req: Request, res: Response) => { - res.writeHead(405).end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Method not allowed.", - }, - id: null, - }), - ); + res + .writeHead(405) + .end(JSON.stringify(createJsonRpcError(-32000, "Method not allowed."))); }); // Session termination not needed in stateless mode app.delete("/mcp", async (req: Request, res: Response) => { - res.writeHead(405).end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Method not allowed.", - }, - id: null, - }), - ); + res + .writeHead(405) + .end(JSON.stringify(createJsonRpcError(-32000, "Method not allowed."))); }); // Main function to start the server in the appropriate mode
README.md+3 −0 modified@@ -61,6 +61,9 @@ When using the STDIO transport or hosting the HTTP transport server, you can pas - `HACKMD_API_TOKEN`: HackMD API Token (Required for all operations) - `HACKMD_API_URL`: (Optional) HackMD API URL (Defaults to https://api.hackmd.io/v1) +Environment variables applied only for the HTTP transport server: +- `ALLOWED_HACKMD_API_URLS`: (Optional) A comma-separated list of allowed HackMD API URLs. The server will reject requests if the provide HackMD API URL is not in this list. If not set, only the default URL (https://api.hackmd.io/v1) is allowed. + > [!CAUTION] > If you are hosting the HTTP transport server with token pre-configured, you should protect your endpoint and implement authentication before allowing users to access it. Otherwise, anyone can access your MCP server while using your HackMD token.
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-g5cg-6c7v-mmpwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59155ghsaADVISORY
- github.com/yuna0x0/hackmd-mcp/commit/43936c78a5bb3dedc74e8f080607a1125caa8c13nvdWEB
- github.com/yuna0x0/hackmd-mcp/releases/tag/v1.5.0ghsaWEB
- github.com/yuna0x0/hackmd-mcp/security/advisories/GHSA-g5cg-6c7v-mmpwnvdWEB
News mentions
0No linked articles in our index yet.