Skip to content

fix(figma): make preview share token read-only; keep a private edit token#75

Open
piaskowyk wants to merge 1 commit into
mainfrom
claude/distracted-visvesvaraya-75cf54
Open

fix(figma): make preview share token read-only; keep a private edit token#75
piaskowyk wants to merge 1 commit into
mainfrom
claude/distracted-visvesvaraya-75cf54

Conversation

@piaskowyk

@piaskowyk piaskowyk commented Jun 11, 2026

Copy link
Copy Markdown
Member

Problem

The Figma plugin used a single token as both the read key (embedded in the preview share URL) and the write key (PUT config / PATCH visibility). Handing the preview link to anyone gave them full edit/delete rights over the project.

Fix

Split the token into two secrets per project:

  • Edit token (private) — the owner's secret. Grants writes (PUT/PATCH) and the owner read. Stored only in the plugin's clientStorage; never put in a URL.
  • Public token (read-only) — the only token in share links, QR codes and deep links. Reads through GET /figma-project/public/:publicToken, which honours visibility (403 when revoked) and can never modify the project.

Only the designer who created the project (and holds the edit token) can modify it; anyone with the public token can read only.

Changes by layer

Server (docs/server)

  • New indexed public_token column; createFigmaProject generates two distinct tokens.
  • GET /figma-project/public/:publicToken — read-only share path, enforces is_public.
  • GET /figma-project/:token — owner read by edit token; serves config even when private and returns the publicToken.
  • PUT / PATCH …/visibility unchanged — still keyed on the secret edit token.
  • public_token backfilled = token for pre-split rows so already-distributed links keep working.

Plugin (figma/src) — stores both tokens per file, builds every share URL / deep link / "copy token" from the public token, recovers the public token from the server for legacy shares.

Preview (figma/preview) — reads through the public route. The mobile app needed no change (it forwards whatever token the QR/deep link carries, now the public one).

Tests

84/84 server tests pass. New coverage proves the security boundary end-to-end:

  • A public-token holder gets 404 on PUT and PATCH (cannot edit or revoke the link); project verified untouched.
  • The public route 404s when handed the edit token; the owner read still serves config while private.
  • The migration backfill (and its WHERE public_token IS NULL guard) is exercised directly.

Caveat

Links already shared before this change remain writable — their token is both edit and public, and it can't be rotated without breaking the owner who still holds it. Every project created from now on is secure. Documented in figma/AGENT_CONTEXT.md.

…oken

The Figma plugin used a single token as both the read key (embedded in the
preview share URL) and the write key (PUT config / PATCH visibility). Anyone
handed the preview link could therefore modify or delete the project.

Split it into two secrets per project:

- edit token (private): the owner's secret, stored only in the plugin's
  clientStorage. Grants writes (PUT/PATCH) and the owner read. Never put in a URL.
- public token (read-only): the only token in share links, QR codes and deep
  links. Reads through GET /figma-project/public/:publicToken, which honours
  visibility (403 when revoked) and can never modify.

Server: add an indexed public_token column, backfilled = token for pre-split
rows so already-distributed links keep working; new owner vs. public read routes;
POST returns both tokens. Plugin: persist both tokens per file, build every share
URL from the public token, recover the public token from the server for legacy
shares. Preview app: read through the public route.

Tests: cover the split end-to-end — a public-token holder gets 404 on PUT and
PATCH (cannot edit or revoke), the owner read still serves config while private,
and the migration backfill is exercised directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@piaskowyk piaskowyk force-pushed the claude/distracted-visvesvaraya-75cf54 branch from 54c1e0d to b50c877 Compare June 11, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant