AppSec audit: authN / authZ / FuzeFront-auth compliance / input validation
Scope audited: all HTTP/event handlers. The entire HTTP surface lives in bridge-server.js (FigmaMcpBridgeServer, default port 3001 / launched on 3015). Other .js files (cursor-mcp-client.js, test.js, analyze-design.js, get-pages.js, search-elements.js, design-system-analyzer.js) are HTTP clients; code.js is the Figma plugin sandbox that fetches the bridge — no server handlers there.
Endpoints (bridge-server.js):
GET /mcp/sse — SSE stream; emits server-info
POST /mcp/request — MCP JSON-RPC (initialize, tools/list, tool calls forwarded to plugin)
POST /plugin/connect — flips figmaConnected = true
GET /plugin/get-requests — returns ALL pending MCP requests
POST /plugin/send-response — submits a response for any requestId
GET /status, GET /health
Findings
CRITICAL — No authentication on any route (bridge-server.js:51-95 handleRequest dispatch; all handle* methods).
Every endpoint is fully unauthenticated. Combined with Access-Control-Allow-Origin: * (bridge-server.js:53) and manifest networkAccess.allowedDomains: ["*"], any web page or local process the user visits can drive the bridge and, through the connected plugin, read/manipulate the user's Figma document (create/delete pages & nodes, exfiltrate document contents). There is no token, no origin allowlist, no localhost-binding enforcement (server listens on all interfaces).
HIGH — Missing object/field-level authZ (BOLA/BOPLA): GET /plugin/get-requests (handlePluginGetRequests, ~line 300) and POST /plugin/send-response (handlePluginSendResponse, ~line 315).
get-requests returns every queued MCP request (Array.from(this.pendingPluginRequests.values())) to any caller — no per-caller scoping. Queued tool payloads / design data leak to any client.
send-response accepts an attacker-chosen requestId and resolves the matching pending MCP request with attacker-controlled content — no binding between the responder and the original requester. This is cross-client response injection / forgery (BOLA on requestId).
POST /plugin/connect lets any caller set the global figmaConnected flag with no shared secret, enabling spoofing of the plugin side.
HIGH — FuzeFront-auth non-compliance (architecture-guidelines §1).
The product implements no authN/authZ and does not integrate FuzeFront for identity. Note: there is no local password store and no self-minted user JWT (no bcrypt/argon2/jwt.sign), so this is not a local-credential/self-token violation — it is absence of the mandated FuzeFront auth. The bridge must sit behind FuzeFront authN/authZ (or, if it is strictly a localhost developer tool, that boundary must be enforced and documented).
MEDIUM — Input validation absent (processMcpRequest ~line 165; handlePluginSendResponse).
Request bodies are JSON.parse'd then fields used unchecked: request.params.name (request.params may be undefined → unhandled crash / DoS), request.method, and { requestId, response } are never schema-validated. No size limits on streamed bodies (req.on('data') concatenation is unbounded → memory DoS).
Concrete fixes
- AuthN: require a per-session bearer token (generated by the plugin at startup, shown in the plugin UI, sent as
Authorization: Bearer); reject all requests lacking it. Bind the listener to 127.0.0.1 only. Replace Access-Control-Allow-Origin: * with an explicit allowlist (or drop CORS for a localhost-only tool) and tighten manifest allowedDomains.
- AuthZ (BOLA/BOPLA): scope
pendingPluginRequests to the authenticated plugin session; get-requests/send-response must only see/affect requests owned by the caller's session. Validate that requestId belongs to the caller before resolving. Gate /plugin/connect behind the same session token.
- FuzeFront compliance: integrate FuzeFront authN/authZ for the control plane, OR formally classify this as a localhost-only dev tool with enforced loopback binding + token, and record the exception per architecture-guidelines §1.
- Input validation: validate JSON-RPC envelope and each handler's body against a schema (method/params/requestId types, required fields); cap request body size; guard
request.params before dereferencing .name.
Severity counts: Critical 1, High 2, Medium 1.
Owner: backend-engineer (do NOT implement here — tracked for the responsible engineer).
AppSec audit: authN / authZ / FuzeFront-auth compliance / input validation
Scope audited: all HTTP/event handlers. The entire HTTP surface lives in
bridge-server.js(FigmaMcpBridgeServer, default port 3001 / launched on 3015). Other.jsfiles (cursor-mcp-client.js,test.js,analyze-design.js,get-pages.js,search-elements.js,design-system-analyzer.js) are HTTP clients;code.jsis the Figma plugin sandbox thatfetches the bridge — no server handlers there.Endpoints (
bridge-server.js):GET /mcp/sse— SSE stream; emits server-infoPOST /mcp/request— MCP JSON-RPC (initialize,tools/list, tool calls forwarded to plugin)POST /plugin/connect— flipsfigmaConnected = trueGET /plugin/get-requests— returns ALL pending MCP requestsPOST /plugin/send-response— submits a response for anyrequestIdGET /status,GET /healthFindings
CRITICAL — No authentication on any route (
bridge-server.js:51-95handleRequestdispatch; allhandle*methods).Every endpoint is fully unauthenticated. Combined with
Access-Control-Allow-Origin: *(bridge-server.js:53) and manifestnetworkAccess.allowedDomains: ["*"], any web page or local process the user visits can drive the bridge and, through the connected plugin, read/manipulate the user's Figma document (create/delete pages & nodes, exfiltrate document contents). There is no token, no origin allowlist, no localhost-binding enforcement (server listens on all interfaces).HIGH — Missing object/field-level authZ (BOLA/BOPLA):
GET /plugin/get-requests(handlePluginGetRequests, ~line 300) andPOST /plugin/send-response(handlePluginSendResponse, ~line 315).get-requestsreturns every queued MCP request (Array.from(this.pendingPluginRequests.values())) to any caller — no per-caller scoping. Queued tool payloads / design data leak to any client.send-responseaccepts an attacker-chosenrequestIdand resolves the matching pending MCP request with attacker-controlled content — no binding between the responder and the original requester. This is cross-client response injection / forgery (BOLA onrequestId).POST /plugin/connectlets any caller set the globalfigmaConnectedflag with no shared secret, enabling spoofing of the plugin side.HIGH — FuzeFront-auth non-compliance (architecture-guidelines §1).
The product implements no authN/authZ and does not integrate FuzeFront for identity. Note: there is no local password store and no self-minted user JWT (no bcrypt/argon2/
jwt.sign), so this is not a local-credential/self-token violation — it is absence of the mandated FuzeFront auth. The bridge must sit behind FuzeFront authN/authZ (or, if it is strictly a localhost developer tool, that boundary must be enforced and documented).MEDIUM — Input validation absent (
processMcpRequest~line 165;handlePluginSendResponse).Request bodies are
JSON.parse'd then fields used unchecked:request.params.name(request.paramsmay beundefined→ unhandled crash / DoS),request.method, and{ requestId, response }are never schema-validated. No size limits on streamed bodies (req.on('data')concatenation is unbounded → memory DoS).Concrete fixes
Authorization: Bearer); reject all requests lacking it. Bind the listener to127.0.0.1only. ReplaceAccess-Control-Allow-Origin: *with an explicit allowlist (or drop CORS for a localhost-only tool) and tighten manifestallowedDomains.pendingPluginRequeststo the authenticated plugin session;get-requests/send-responsemust only see/affect requests owned by the caller's session. Validate thatrequestIdbelongs to the caller before resolving. Gate/plugin/connectbehind the same session token.request.paramsbefore dereferencing.name.Severity counts: Critical 1, High 2, Medium 1.
Owner: backend-engineer (do NOT implement here — tracked for the responsible engineer).