CVE-2025-53967
Description
Framelink Figma MCP Server before 0.6.3 allows an unauthenticated remote attacker to execute arbitrary operating system commands via a crafted HTTP POST request with shell metacharacters in input that is used by a fetchWithRetry curl command. The vulnerable endpoint fails to properly sanitize user-supplied input, enabling the attacker to inject malicious commands that are executed with the privileges of the MCP process. Exploitation requires network access to the MCP interface.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
figma-developer-mcpnpm | < 0.6.3 | 0.6.3 |
Patches
2927f2c1984c17f4b5859454bUpdates to validate user input, run HTTP server on localhost only (#246)
6 files changed · +42 −17
.changeset/fluffy-planes-argue.md+5 −0 added@@ -0,0 +1,5 @@ +--- +"figma-developer-mcp": patch +--- + +Updates to validate user input, run HTTP server on localhost only
ROADMAP.md+5 −2 modified@@ -22,6 +22,10 @@ _High impact, foundational improvements_ - [ ] Return data on animations / transitions - [?] State management hints +### Parsing Logic + +- [ ] Inline variables that only show up once, and keep global vars only for variables that are reused + ### Image & Asset Handling - [ ] **Fix masked / cropped image exports** @@ -74,8 +78,7 @@ _Improving usability and integration_ ### Performance & Reliability - [ ] **Better error handling** - - [ ] Retry logic for API failures - - [ ] Graceful degradation + - [x] Retry logic for API failures - [ ] Detailed error messages which the LLM can expand on for users ### Documentation & Testing
src/mcp/tools/download-figma-images-tool.ts+9 −4 modified@@ -12,7 +12,7 @@ const parameters = { nodeId: z .string() .regex( - /^I?\d+:\d+(?:;\d+:\d+)*$/, + /^I?\d+[:|-]\d+(?:;\d+[:|-]\d+)*$/, "Node ID must be like '1234:5678' or 'I5666:180910;1:10515;1:10336'", ) .describe("The ID of the Figma image node to fetch, formatted as 1234:5678"), @@ -73,14 +73,19 @@ export type DownloadImagesParams = z.infer<typeof parametersSchema>; // Enhanced handler function with image processing support async function downloadFigmaImages(params: DownloadImagesParams, figmaService: FigmaService) { try { - const { fileKey, nodes, localPath, pngScale = 2 } = params; + const { fileKey, nodes, localPath, pngScale = 2 } = parametersSchema.parse(params); // Process nodes: collect unique downloads and track which requests they satisfy const downloadItems = []; const downloadToRequests = new Map<number, string[]>(); // download index -> requested filenames const seenDownloads = new Map<string, number>(); // uniqueKey -> download index - for (const node of nodes) { + for (const rawNode of nodes) { + const { nodeId: rawNodeId, ...node } = rawNode; + + // Replace - with : in nodeId for our query—Figma API expects : + const nodeId = rawNodeId?.replace(/-/g, ":"); + // Apply filename suffix if provided let finalFileName = node.fileName; if (node.filenameSuffix && !finalFileName.includes(node.filenameSuffix)) { @@ -122,7 +127,7 @@ async function downloadFigmaImages(params: DownloadImagesParams, figmaService: F } else { // Rendered nodes are always unique const downloadIndex = downloadItems.length; - downloadItems.push({ ...downloadItem, nodeId: node.nodeId }); + downloadItems.push({ ...downloadItem, nodeId }); downloadToRequests.set(downloadIndex, [finalFileName]); } }
src/mcp/tools/get-figma-data-tool.ts+10 −2 modified@@ -8,14 +8,19 @@ import { Logger, writeLogs } from "~/utils/logger.js"; const parameters = { fileKey: z .string() + .regex(/^[a-zA-Z0-9]+$/, "File key must be alphanumeric") .describe( "The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/...", ), nodeId: z .string() + .regex( + /^I?\d+[:|-]\d+(?:;\d+[:|-]\d+)*$/, + "Node ID must be like '1234:5678' or 'I5666:180910;1:10515;1:10336'", + ) .optional() .describe( - "The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided", + "The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided. Use format '1234:5678' or 'I5666:180910;1:10515;1:10336' for multiple nodes.", ), depth: z .number() @@ -35,7 +40,10 @@ async function getFigmaData( outputFormat: "yaml" | "json", ) { try { - const { fileKey, nodeId, depth } = params; + const { fileKey, nodeId: rawNodeId, depth } = parametersSchema.parse(params); + + // Replace - with : in nodeId for our query—Figma API expects : + const nodeId = rawNodeId?.replace(/-/g, ":"); Logger.log( `Fetching ${depth ? `${depth} layers deep` : "all layers"} of ${
src/server.ts+1 −1 modified@@ -176,7 +176,7 @@ export async function startHttpServer(port: number, mcpServer: McpServer): Promi } }); - httpServer = app.listen(port, () => { + httpServer = app.listen(port, "127.0.0.1", () => { Logger.log(`HTTP server listening on port ${port}`); Logger.log(`SSE endpoint available at http://localhost:${port}/sse`); Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
src/utils/fetch-with-retry.ts+12 −8 modified@@ -1,8 +1,8 @@ -import { exec } from "child_process"; +import { execFile } from "child_process"; import { promisify } from "util"; import { Logger } from "./logger.js"; -const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); type RequestOptions = RequestInit & { /** @@ -35,12 +35,12 @@ export async function fetchWithRetry<T extends { status?: number }>( // -S: Show errors in stderr // --fail-with-body: curl errors with code 22, and outputs body of failed request, e.g. "Fetch failed with status 404" // -L: Follow redirects - const curlCommand = `curl -s -S --fail-with-body -L ${curlHeaders.join(" ")} "${url}"`; + const curlArgs = ["-s", "-S", "--fail-with-body", "-L", ...curlHeaders, url]; try { // Fallback to curl for corporate networks that have proxies that sometimes block fetch - Logger.log(`[fetchWithRetry] Executing curl command: ${curlCommand}`); - const { stdout, stderr } = await execAsync(curlCommand); + Logger.log(`[fetchWithRetry] Executing curl with args: ${JSON.stringify(curlArgs)}`); + const { stdout, stderr } = await execFileAsync("curl", curlArgs); if (stderr) { // curl often outputs progress to stderr, so only treat as error if stdout is empty @@ -81,14 +81,18 @@ export async function fetchWithRetry<T extends { status?: number }>( } /** - * Converts HeadersInit to an array of curl header arguments. + * Converts HeadersInit to an array of curl header arguments for execFile. * @param headers Headers to convert. - * @returns Array of strings, each a curl -H argument. + * @returns Array of strings for curl arguments: ["-H", "key: value", "-H", "key2: value2"] */ function formatHeadersForCurl(headers: Record<string, string> | undefined): string[] { if (!headers) { return []; } - return Object.entries(headers).map(([key, value]) => `-H "${key}: ${value}"`); + const headerArgs: string[] = []; + for (const [key, value] of Object.entries(headers)) { + headerArgs.push("-H", `${key}: ${value}`); + } + return headerArgs; }
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
6- github.com/advisories/GHSA-gxw4-4fc5-9gr5ghsaADVISORY
- github.com/GLips/Figma-Context-MCP/commit/7f4b5859454b0567c2121ff22c69a0344680b124ghsaWEB
- github.com/GLips/Figma-Context-MCP/security/advisories/GHSA-gxw4-4fc5-9gr5ghsaWEB
- github.com/GLips/Figma-Context-MCP/blob/96b3852669c5eed65e4a6e20406c25504d9196f2/src/utils/fetch-with-retry.tsnvd
- github.com/GLips/Figma-Context-MCP/releases/tag/v0.6.3nvd
- www.imperva.com/blog/another-critical-rce-discovered-in-a-popular-mcp-server/nvd
News mentions
0No linked articles in our index yet.