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.
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.
Shopify Storefront
Product listing with a "Customize Label" CTA per product. Hosts the designer tool at /designer-tool on the same origin.
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.
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.
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.
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 });
}
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.
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.
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.
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.
Double protection on all mutation endpoints
- Set
SameSite=Stricton the Shopify session cookie (prevents cross-site submission entirely for modern browsers). - Implement the double-submit cookie pattern: include a
csrf-tokenin 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.
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.
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.
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.
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
destclaim 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
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.
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.
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.
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 externalhref/srcreferences. - 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 ofshopify-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'
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.
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.
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'],
});
}
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 |
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.