VYPR
High severity8.0NVD Advisory· Published Oct 8, 2025· Updated Apr 15, 2026

CVE-2025-53967

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.

PackageAffected versionsPatched versions
figma-developer-mcpnpm
< 0.6.30.6.3

Patches

2
7f4b5859454b

Updates to validate user input, run HTTP server on localhost only (#246)

https://github.com/GLips/Figma-Context-MCPGraham LipsmanSep 29, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.