The plug-and-play approach is elegant on paper — same domain, shared session, no extra login. In practice, it turns every Shopify customer cookie into a potential attack vector for your entire label printing backend.

01

System Overview

The system described in this article is a three-part platform: a Shopify storefront where customers browse and select products, a FabricJS Canvas Designer Tool where they customize label artwork, and a React Admin Panel where operators manage templates, SVG assets, safe zones, and bleed zones for each product.

01

Shopify Storefront

Product listing with a "Customize Label" CTA per product. Hosts the designer tool at /designer-tool on the same origin.

02

FabricJS Canvas Editor

Embedded at shopify-product.com/designer-tool. Loads SVG templates, applies safe/bleed zone overlays, and saves the customer's design JSON.

03

React Admin Panel

Rendered inside the Shopify Admin via an <iframe>. Manages all templates, asset mappings, and canvas constraints per product.

The two architectural decisions that define this system's security posture are: hosting the designer tool on the same origin as the Shopify store, and embedding the admin panel inside Shopify Admin as an iframe. Both are reasonable, widely used patterns — but each one introduces a specific class of vulnerability that must be explicitly handled.

02

Architecture Decisions & Their Rationale

Decision 1 — Designer Tool on Same Origin

Hosting the designer at shopify-product.com/designer-tool is the simplest integration path. The active Shopify session cookie is automatically available, no OAuth redirect is needed, and the customer never notices they've moved to a different application. This is the "plug-and-play" approach.

It works because browsers automatically attach cookies to any request made to the same origin — including requests made from within the FabricJS canvas application when it calls your backend API. The customer is already authenticated with Shopify, so your backend can verify their identity without any additional login step.

!

The convenience is real — but so is the risk. The Shopify session cookie was designed to authenticate storefront browsing, not to authorize design operations. Reusing it directly means any storefront session has implicit access to your entire design API surface.

Decision 2 — Admin Panel as Shopify Admin Iframe

Embedding the React Admin Panel inside Shopify Admin via an <iframe> with Content-Security-Policy: frame-ancestors restricting to the Shopify origin is a clean, well-supported pattern. Merchants see the admin panel as a native part of their Shopify experience, and you avoid building a separate authentication system for operators.

Shopify's App Bridge provides a postMessage-based communication channel between the parent Shopify Admin page and the embedded iframe, allowing the admin panel to receive context (store URL, access token) from the host.

03

The Correct Authentication Flow

Before examining what can go wrong, it's useful to understand the intended auth flow. The system involves three distinct authentication contexts that must never bleed into one another.

Context Who Mechanism Scope
Storefront Shopify customer Shopify session cookie Browse & purchase only
Designer Shopify customer Scoped JWT issued by your backend Design operations for specific product + customer
Admin Shopify merchant/operator Shopify Admin JWT (App Bridge) Template & asset management only
Service Backend ↔ Shopify API API key + secret (env vars) Admin API calls, webhook verification

The critical step that most implementations skip: when the customer navigates to the designer tool, the backend should exchange the Shopify session for a purpose-built JWT — one that encodes { customerId, productId, designScope, exp: +30min }. All designer API calls use this JWT exclusively. The Shopify session cookie is never trusted by the design API.

// Token exchange endpoint — POST /designer/init
async function initDesignerSession(req, res) {
  // 1. Verify the incoming Shopify session
  const shopifyCustomer = await verifyShopifySession(req.cookies.session);

  // 2. Issue a scoped, short-lived JWT
  const token = jwt.sign({
    customerId: shopifyCustomer.id,
    productId:  req.body.productId,
    scope:      'design:write',
  }, process.env.JWT_SECRET, { expiresIn: '30m' });

  // 3. Mark handoff token as consumed (prevent replay)
  await db.handoffTokens.consume(req.body.handoffToken);

  res.json({ designerToken: token });
}
04

Security Breaches — What Goes Wrong

The following seven vulnerabilities are not theoretical — they are the direct, predictable consequence of the two architectural decisions described above when implemented without explicit mitigations. Each one is specific to this system's design.

Breach #1 — Cookie Scope Bleed

Shopify session cookie grants access to all designer API routes

Because the designer tool lives at shopify-product.com/designer-tool, the browser attaches the Shopify session cookie to every API request the designer makes. If your backend trusts this cookie to authorize design operations, then any valid Shopify storefront session — including a freshly created one with no purchase history — has full write access to your design save, export, and template endpoints. This is an unintended implicit trust grant.

Fix — Token Exchange on Init

Issue a scoped JWT at designer initialization

Never allow the Shopify session cookie to reach your design API. On GET /designer-tool?productId=X, your backend verifies the Shopify session, then issues a short-lived JWT encoding the customer ID, product ID, and a design:write scope. The FabricJS app stores this in memory (never localStorage) and sends it as a Bearer token. All design API routes require this JWT.

Breach #2 — CSRF (Cross-Site Request Forgery)

Cookie authentication on a same-origin route enables cross-site state mutations

Any route authenticated by a cookie is a CSRF target. A malicious third-party site can construct a form that submits to shopify-product.com/designer-tool/api/save, and the victim's browser will automatically attach their Shopify session cookie. Because the request appears identical to a legitimate one from the server's perspective, it will be processed. This can be used to overwrite a customer's saved design, trigger PDF exports, or exhaust server resources.

Fix — SameSite + CSRF Tokens

Double protection on all mutation endpoints

  • Set SameSite=Strict on the Shopify session cookie (prevents cross-site submission entirely for modern browsers).
  • Implement the double-submit cookie pattern: include a csrf-token in both a cookie and a request header on all POST/PUT/DELETE endpoints. Your server validates that both values match.
  • The scoped JWT approach from Breach #1's fix also inherently mitigates CSRF — Bearer tokens are not automatically attached by browsers, so cross-site requests will not carry the JWT.
Breach #3 — Unvalidated postMessage (Admin iframe)

Any origin can inject messages into your admin panel

Shopify App Bridge uses window.postMessage to pass admin context (session token, store details) from the parent Shopify Admin page to your embedded iframe. If your admin panel's message listener does not strictly validate event.origin, any other page that can reference your iframe — including browser extensions, other Shopify apps, or malicious scripts on compromised merchant devices — can inject fake messages. A well-crafted fake message could cause the admin panel to make API calls with spoofed context, or to display incorrect product mappings.

Fix — Strict Origin Validation

Validate every incoming postMessage event

window.addEventListener('message', (event) => {
  // Reject anything not from Shopify Admin
  const allowed = [
    'https://your-store.myshopify.com',
    'https://admin.shopify.com'
  ];
  if (!allowed.includes(event.origin)) return;

  // Also validate message structure before acting
  if (!event.data?.type || !event.data?.payload) return;

  handleAdminMessage(event.data);
});

Also use a specific target origin when sending messages out: never pass '*' as the second argument to postMessage.

Breach #4 — CSP frame-ancestors ≠ Authentication

Restricting the iframe origin does not protect your admin API routes

Content-Security-Policy: frame-ancestors https://admin.shopify.com correctly prevents your admin panel from being embedded on other domains. However, this is a rendering constraint, not an authorization control. A merchant can still open the raw admin panel URL directly in their browser (without going through the iframe), and every admin API endpoint will be accessible without any Shopify session validation. Worse, the CSP header must be set as an HTTP response header — a <meta> CSP tag is ignored by browsers for frame-ancestors.

Fix — Server-side Shopify Admin JWT verification

Every admin API call independently verifies identity

  • Use Shopify App Bridge to get a session token in the iframe. This is a short-lived, signed JWT issued by Shopify.
  • Send this token as a Bearer token on all admin API requests.
  • Verify the JWT on your server using Shopify's public key — confirm the dest claim matches your store URL and the token has not expired.
  • Set the CSP header server-side: Content-Security-Policy: frame-ancestors https://your-store.myshopify.com https://admin.shopify.com
Breach #5 — IDOR (Insecure Direct Object Reference)

A customer can access or overwrite any other customer's design by guessing an ID

If your design retrieval endpoint is GET /api/designs/:id and you only check that the request is authenticated (valid JWT), a logged-in customer can enumerate design IDs — sequential integers are especially vulnerable — and retrieve, modify, or delete other customers' label designs. This is the most common real-world vulnerability in multi-tenant design tools.

Fix — Ownership check on every query

Bind every design query to the authenticated customer

// ❌ WRONG — authenticates but doesn't authorize
SELECT * FROM designs WHERE id = $1

// ✅ CORRECT — ownership enforced at query level
SELECT * FROM designs
WHERE id = $1
  AND owner_id = $2  -- $2 comes from the verified JWT, never from the request body

Use UUIDs instead of sequential integers for design IDs to eliminate enumeration as an attack vector even if the ownership check is accidentally missing.

Breach #6 — SVG XSS via Admin Upload

A malicious SVG uploaded by an admin executes scripts in every customer's browser

SVG is XML, and XML supports <script> tags, onload event handlers, <use href="external-resource"> references, and embedded data: URIs. An SVG file uploaded by an admin — or by an attacker who has compromised an admin account — will be served to every customer who opens a product's designer. If the SVG is served from the same origin as the Shopify store, any scripts inside it have access to document.cookie, localStorage, and the ability to make authenticated requests on behalf of the customer.

Fix — Sanitize server-side + isolate origin

Two independent defenses in depth

  • Sanitize on upload: Run every uploaded SVG through DOMPurify (Node.js isomorphic-dompurify) before storing it. Strip all script elements, event handler attributes (on*), and external href / src references.
  • Serve from a separate subdomain: Host all SVG assets at assets.cdn-yourbrand.com (or an S3 bucket with a CDN). Scripts inside an SVG served from a different origin cannot access cookies or storage of shopify-product.com. This is your safety net if sanitization ever misses something.
  • Set a restrictive CSP on the asset subdomain: Content-Security-Policy: default-src 'none'; img-src 'self'; style-src 'self'
Breach #7 — Handoff Token Leakage

The signed URL passed from Shopify to the designer persists in logs, history, and headers

The URL used to transition from Shopify to the designer tool — /designer-tool?productId=X&token=<signed> — appears in browser history, server access logs, analytics platforms (like Google Analytics or Mixpanel), Shopify's referrer logs, and potentially in the Referer HTTP header when the designer tool loads external resources. If this token has a long TTL and is not single-use, anyone who obtains it (e.g. a shared device, a leaked log file) can open the designer as that customer.

Fix — Short TTL + single-use nonce

Make the handoff token ephemeral and non-replayable

  • Set a 5-minute expiry on the handoff token.
  • Store a nonce in your database when the token is issued. On first use, the backend marks the nonce as consumed. A second request with the same token is rejected with 401.
  • After consumption, the designer receives the scoped JWT (from the token exchange step) and all subsequent requests use that JWT — the handoff URL is no longer needed and cannot be replayed.
05

Complete Security Architecture

The fixes described above are not independent patches — they form a layered defense. Here is how they compose into a coherent architecture.

Layer 1 — Perimeter: Headers & CSP

Every response from your backend should include the following headers. These prevent the most common browser-level attacks before any application code runs.

# Required HTTP response headers for all routes
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  connect-src 'self' https://api.shopify.com;
  img-src 'self' https://assets.cdn-yourbrand.com data:;
  frame-ancestors 'none';  # storefront pages — no framing

# Admin panel ONLY — override frame-ancestors
Content-Security-Policy:
  frame-ancestors https://your-store.myshopify.com https://admin.shopify.com;

X-Frame-Options: DENY           # legacy browser fallback for storefront
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin   # prevents token leakage in Referer header

Layer 2 — Authentication: Three Contexts, Three Tokens

The designer tool must never make API calls authenticated by the Shopify cookie. The admin panel must never make API calls authenticated by a customer JWT. The token exchange endpoint is the explicit boundary between these contexts — it is the most important endpoint in the entire system.

// Middleware stack for designer API routes
router.use('/api/design', [
  verifyBearerJWT,            // must be the scoped designer JWT
  requireScope('design:write'), // not just any valid JWT
  rateLimitByCustomer(100),   // 100 req/hour per customer
]);

// Middleware stack for admin API routes
router.use('/api/admin', [
  verifyShopifyAdminJWT,      // App Bridge session token
  requireRole('merchant'),   // not customer scope
  rateLimitByStore(500),      // 500 req/hour per store
]);

Layer 3 — Authorization: Ownership at the Data Layer

Authentication confirms identity. Authorization confirms permission. Every design operation must enforce ownership at the database query level — not in application code that might be bypassed. Use UUIDs as primary keys for all user-owned resources.

Layer 4 — Input: Sanitize at the Boundary

All user-supplied content — SVG files, FabricJS JSON, product IDs from query strings — must be sanitized and validated on the server before being stored or processed. Client-side validation is UX, not security. Never pass raw FabricJS JSON directly to a PDF renderer.

// SVG sanitization on upload
import createDOMPurify from 'isomorphic-dompurify';
import { JSDOM } from 'jsdom';

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

function sanitizeSVG(svgString) {
  return DOMPurify.sanitize(svgString, {
    USE_PROFILES: { svg: true, svgFilters: true },
    FORBID_TAGS: ['script', 'use'],
    FORBID_ATTR: ['href', 'xlink:href', 'src'],
  });
}
06

Final Security Checklist

Before deploying, every item below should have a confirmed implementation owner and a test case in your security review.

Category Item Status
Auth Token exchange on designer init — Shopify cookie never reaches design API Required
Auth Admin Panel uses Shopify App Bridge session token, verified server-side Required
Auth Handoff token is single-use with 5-minute TTL Required
CSRF SameSite=Strict on session cookie + CSRF token on all mutations Required
CSP frame-ancestors set as HTTP header (not meta tag) for admin panel Required
CSP Referrer-Policy: strict-origin on all pages that carry tokens in URL Required
postMessage event.origin validated against allowlist on every message listener Required
Authorization Ownership check (AND owner_id = jwt.customerId) on all design queries Required
Authorization Design IDs use UUIDs, not sequential integers Strongly recommended
Input SVG files sanitized with DOMPurify server-side on every upload Required
Input SVG assets served from a separate CDN subdomain Required
Input FabricJS JSON schema-validated before PDF export render Strongly recommended
Transport HTTPS everywhere with HSTS; no mixed-content Required
Rate Limiting Design save and export endpoints rate-limited per customer session Strongly recommended
Webhooks Shopify webhook HMAC signature verified on all webhook endpoints Required
i

One important note on the plug-and-play approach: none of these vulnerabilities make the same-origin or iframe architecture a bad choice. They are inherent to the model and entirely fixable. The system is well-suited to this architecture — you simply need to be explicit about every trust boundary rather than relying on the browser's implicit behavior.