Figma Plugin Security Best Practices for Protecting User Data

JP
Jordan Pham
UX/UI Designer & Plugin Developer | 7+ Years Experience

Most Figma plugins that mishandle user data aren’t built by anyone with bad intentions. The exposure usually comes from a default left unchanged, a manifest field copied from a tutorial without being fully understood, or a third-party SDK bundled in for analytics that quietly collects far more than anyone on the team realized. That’s the uncomfortable fact worth sitting with before diving into any specific fix: insecure plugin behavior is rarely the result of a deliberate decision. It’s the result of nobody looking closely enough at what the plugin was already doing.

An Internal Audit Nobody Expected to Find Anything

A design ops team at a mid-sized product company asked me to review a handful of internal plugins before rolling them out company-wide as approved tools. Nothing about the request suggested urgency. These were small utilities — a color palette syncer, a naming convention checker, a component usage reporter — built by two designers with light coding experience over the course of a few sprints. The plan was a quick sign-off so the plugins could go on the internal-approved list and stop living in people’s personal “development” tab.

The naming checker and the palette syncer passed review without much comment. The component usage reporter didn’t.

That plugin’s job was straightforward: scan a file, tally which components appeared where, and send a summary back to a dashboard so the design system team could see adoption rates across projects. Reasonable feature, reasonable use case. The problem showed up when I opened the network panel while the plugin ran a scan on a test file containing draft product copy and a few placeholder customer names.

The plugin wasn’t just sending component IDs and counts. It was sending full text-layer contents alongside them.

Why This Happened, and Why It’s Common

Tracing the code back, the culprit was a logging utility bolted on early in development to help debug why component counts occasionally came out wrong. The developer had set it to capture the full node tree for any frame containing a mismatch, intending to remove the verbose logging before shipping. That removal never happened. The logging call stayed wired into the same function that sent data to the dashboard, and once the debugging phase ended, nobody revisited whether the payload still needed to include raw layer content.

This is the pattern worth internalizing: most Figma plugin data leaks aren’t the product of malicious network calls hidden in obfuscated code. They’re the product of debug-era logic that outlives its debugging purpose, running quietly in a production plugin that dozens of designers now trust with their files.

The manifest.json for this plugin listed a single domain under networkAccess.allowedDomains, which on its face looked properly scoped — one endpoint, clearly named, clearly belonging to the internal dashboard. That’s exactly why nobody flagged it during earlier informal reviews. A tightly scoped domain list reads as secure. It says nothing about what data is actually traveling to that domain, which is the piece manifest configuration alone can never verify.

Reading the Manifest Isn’t the Same as Auditing the Payload

This is where the audit shifted from a manifest review to a behavioral one. Domain allowlisting in networkAccess restricts where a plugin can send requests, and that matters — it prevents a compromised or careless plugin from phoning home to an arbitrary third-party server. But it does nothing to restrict what gets included in the request body once a connection to an allowed domain is permitted. A plugin can be perfectly scoped on the network layer and still ship an entire document’s text content to its own legitimate backend without anyone intending that outcome.

The practical fix here was to intercept the payload construction directly: log every field being serialized before a request fires, during a test run against a file with obviously sensitive placeholder content, and check each field against a simple question — does the dashboard’s actual feature set require this? Component IDs: yes, that’s the entire point of the tool. Frame names: arguably useful for context, kept after discussion. Full text-node contents: no functional reason at all. It was leftover debug scaffolding, not a deliberate design decision, and it was removed within the hour once identified.

Client Storage Versus Network Storage, and Why the Distinction Matters More Than It Seems

While going through the fix, the team asked a fair follow-up question: should the usage data even leave the file at all, or could figma.clientStorage handle it locally instead? The answer depended on the actual feature requirement, which is the right frame for that decision generally.

figma.clientStorage persists data locally per plugin, scoped to the user’s Figma account on that machine, and never touches the network unless the plugin code explicitly sends it somewhere. It’s the right choice for preferences, cached settings, or any data that only needs to exist for that one user’s convenience. It is the wrong choice the moment a feature requires aggregating data across multiple users or files — which is precisely what this dashboard needed, since design system adoption reporting only makes sense as a cross-team rollup.

The lesson generalizes cleanly: reach for local storage by default, and treat any move to network transmission as a decision that needs its own explicit justification, not an assumption baked in from the start. If a feature can be satisfied entirely with clientStorage, sending that same data over the network adds an attack surface and a compliance question that didn’t need to exist.

The Message-Passing Boundary Deserves the Same Scrutiny

Figma plugins run their logic in a sandboxed main thread with access to the document, while the UI renders in an iframe that talks to that sandbox through postMessage. This boundary gets far less attention during security review than network calls do, but it carries similar risk. Any data passed from the sandbox to the UI iframe is data that’s now reachable by whatever code runs inside that iframe — including third-party scripts, if the UI was built with an embedded analytics tag or a chat widget pulled in from an external package.

During this same audit, a second, smaller issue surfaced: the plugin’s UI iframe loaded a customer support widget script from an external CDN, unrelated to the plugin’s core function, added by a developer who wanted an easy feedback channel for internal users. That widget had access to the DOM of a UI panel that, moments earlier, had received full node data from the sandbox for rendering purposes. Nothing malicious was happening there, but the exposure was structurally identical to a supply-chain risk: a third-party script sitting in the same execution context as document data, with no barrier preventing it from reading that data if the widget’s own code ever changed. The widget was removed. If a support channel was genuinely needed, it belonged behind a link, not an embedded script running inside the plugin’s data path.

A Working Checklist That Came Out of This Audit

The fixes across all three plugins eventually consolidated into a short list the team now runs before approving any plugin for internal use:

CheckWhat to Verify
Manifest network scopenetworkAccess.allowedDomains lists only domains the plugin needs, nothing broader
Payload contentEvery field sent over the network has a clear, current functional purpose
Storage choiceclientStorage used by default; network storage only when cross-user aggregation is required
Debug codeLogging or diagnostic calls added during development are removed, not just disabled
Third-party scripts in UINo external script runs in the same iframe context as sensitive document data
Data minimizationOnly fields the feature actually consumes are collected, never the full node tree “just in case”

None of these checks require specialized tooling. A browser’s network panel, a careful read of manifest.json, and a willingness to trace every field in an outgoing request back to a concrete feature requirement cover most of what this audit uncovered.

What Changed After the Fix Shipped

The component usage reporter went back into rotation about a week later, with the payload trimmed to component IDs, frame names, and file identifiers — nothing resembling document content. The dashboard feature it powered kept working exactly as before, because none of the removed fields had ever been used by the reporting logic in the first place. That’s usually how these fixes go: the sensitive data being collected typically isn’t load-bearing for the feature it’s attached to. It’s just there, unexamined, until someone looks.

The team folded a lightweight version of the checklist above into their plugin review process going forward, checking new internal tools before they reach general use rather than trusting a scoped networkAccess entry as sufficient proof of safety on its own.

Have you audited what your own plugins send over the network recently, or checked whether debug logging from early development is still quietly running in production? It’s worth ten minutes with the network panel open on a test file to find out.

About the Author

Jordan Pham is a UX/UI designer and Figma plugin developer with 7 years of design experience and several published plugins on the Figma Community, used by thousands of designers.