MCP Server Security · Performance APIs · Long Animation Frames / LoAF

MCP server Long Animation Frames API security — LoAF script URL disclosure, third-party script enumeration, main thread surveillance, and execution fingerprinting

The Long Animation Frames API (LoAF, shipping Chrome 123+ and enabled by default) fires a PerformanceLongAnimationFrameTiming entry for every rendering frame that takes more than 50ms. Each entry carries a scripts[] array where each element exposes the sourceURL of the blocking script (its exact CDN URL), the invoker name (the event handler, observer callback, or timer function that triggered execution), the invokerType, the executionStart timestamp, and the forcedStyleAndLayoutDuration. MCP tools subscribe to LoAF entries to enumerate every third-party analytics and tag-management script on the page by CDN URL, identify the exact function names of authentication and payment handlers, version-fingerprint deployed libraries from CDN path patterns, and correlate long frames with user interaction events to reconstruct expensive operation timelines — all without any permission gate and without making a single network request.

Long Animation Frames API attack surface

LoAF fieldWhat it exposesAttack relevance
scripts[].sourceURLCDN URL of every blocking JS fileThird-party script enumeration, library version fingerprint
scripts[].invokerHandler function name that triggered the long taskAuth/payment handler identification, code structure inference
scripts[].invokerType'event-listener' | 'user-callback' | 'resolve-promise' | 'reject-promise' | 'classic-script' | 'module-script'Distinguishes user interactions from background tasks
scripts[].executionStartTimestamp when the script began executingCorrelate with Resource Timing responseEnd for auth flow timing
scripts[].forcedStyleAndLayoutDurationTime spent in forced style recalculation/layoutIdentifies DOM-heavy operations (table renders, large list updates)
blockingDurationTotal ms the frame blocked the main threadClassifies operation weight for expensive auth/payment detection
startTime + durationFrame timing windowCorrelate with Event Timing entries to match long frames to specific interactions

Attack 1: Third-party script enumeration via sourceURL

Every analytics SDK, tag manager, A/B testing platform, session recorder, ad network, and customer data platform that loads on a page will produce at least one LoAF entry during initialization — their initialization scripts are almost always the heaviest synchronous workloads on the page. The scripts[].sourceURL field exposes the exact CDN URL of the script file that caused each long frame:

const obs = new PerformanceObserver(list => {
  const scripts = new Set();
  for (const entry of list.getEntries()) {
    for (const script of entry.scripts) {
      if (script.sourceURL) scripts.add(script.sourceURL);
    }
  }
  // scripts now contains:
  // "https://cdn.segment.io/analytics.js/v1/XXX/analytics.min.js"
  // "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX"
  // "https://static.hotjar.com/c/hotjar-12345.js?sv=6"
  // "https://js.stripe.com/v3/"
  // "https://cdn.optimizely.com/js/12345678901.js"
  navigator.sendBeacon('/c', JSON.stringify([...scripts]));
});
obs.observe({ type: 'long-animation-frame', buffered: true });

This enumeration reveals the site's complete third-party technology stack: which analytics vendor, which A/B testing platform, which session recorder, which payment processor, and which customer data platform. This intelligence is valuable for competitive analysis, targeted exploitation (knowing the CDN URL allows version-specific CVE matching), and social engineering (impersonating the detected vendors in phishing campaigns directed at the site's team).

Version fingerprinting. Many CDN URLs encode the library version or bundle ID directly in the path. cdn.optimizely.com/js/12345678901.js uniquely identifies the Optimizely project; hotjar-12345.js exposes the Hotjar site ID. Versioned SDKs (stripe.js/v3/, segment.io/v1/) confirm which major API version is deployed, enabling targeted exploit research for known version-specific vulnerabilities.

Attack 2: Authentication and payment handler identification via invoker

When a user clicks a login button, submits a payment form, or triggers a multi-factor authentication flow, the resulting JavaScript execution often takes 100–500ms — qualifying it as a long animation frame. The scripts[].invoker field names the exact function that was called:

// LoAF entry when user clicks "Sign In":
{
  "duration": 340,
  "blockingDuration": 290,
  "scripts": [
    {
      "sourceURL": "https://site.com/static/auth.bundle.a3f9c.js",
      "invoker": "HTMLButtonElement.onclick",
      "invokerType": "event-listener",
      "executionStart": 12483.7,
      "forcedStyleAndLayoutDuration": 45.2
    },
    {
      "sourceURL": "https://cdn.auth0.com/js/auth0-spa-js/2.1.3/auth0-spa-js.production.min.js",
      "invoker": "Auth0Client.loginWithPopup",
      "invokerType": "resolve-promise",
      "executionStart": 12521.4
    }
  ]
}

From this single LoAF entry, an MCP tool infers: (1) the user is logging in right now, (2) the site uses Auth0 SPA SDK version 2.1.3 (exact version from CDN URL), (3) the authentication flow uses loginWithPopup rather than redirect flow. The invokerType: "resolve-promise" on the Auth0 entry confirms it's executing in the promise resolution microtask queue — the authentication request has already been sent.

Attack 3: Long frame correlation with Event Timing for interaction attribution

LoAF entries and Event Timing entries both record startTime and duration. When a user interaction (Event Timing entry) overlaps in time with a long animation frame (LoAF entry), the two entries refer to the same user action. Correlating them yields: what the user did (from Event Timing's target), which scripts ran in response (from LoAF's scripts[]), and how long the operation took:

const loafEntries = [];
const eventEntries = [];

new PerformanceObserver(list => {
  loafEntries.push(...list.getEntries());
}).observe({ type: 'long-animation-frame', buffered: true });

new PerformanceObserver(list => {
  for (const event of list.getEntries()) {
    const matching = loafEntries.filter(loaf =>
      loaf.startTime <= event.startTime + event.duration &&
      loaf.startTime + loaf.duration >= event.startTime
    );
    exfiltrate({
      action: event.name,
      target: event.target?.id,
      scripts: matching.flatMap(l => l.scripts.map(s => s.invoker))
    });
  }
}).observe({ type: 'event', durationThreshold: 0, buffered: true });

This pattern produces a complete audit log: user clicked element X → auth handler Y ran → Auth0 SDK version Z called → 340ms later, frame unblocked. Without ever touching a network request log or server-side audit trail.

Attack 4: Forced layout detection for DOM-heavy feature identification

The scripts[].forcedStyleAndLayoutDuration field measures how long a script spent forcing synchronous style recalculation and layout. High values (>20ms) indicate operations that read layout properties (offsetHeight, getBoundingClientRect) after mutating the DOM — a common pattern in virtualized list renderers, table components, and animation libraries. An MCP tool identifies which features of the page trigger forced layouts, revealing which components handle large amounts of data (user table = many rows = healthcare patient list, financial transaction history, etc.).

No permission required, Chrome 123+ by default. LoAF is enabled in Chrome 123+ without any flag, permission dialog, or Permissions Policy opt-in. Safari and Firefox don't implement it yet, but Chrome's market share makes this a real-world threat for sites serving Chrome users — which is most sites.

SkillAudit findings for Long Animation Frames API

HIGH
LoAF observer with sourceURL exfiltration — A PerformanceObserver subscribing to 'long-animation-frame' that reads scripts[].sourceURL and sends it over the network. Exposes the complete third-party technology stack including version identifiers and CDN path patterns.
HIGH
LoAF + Event Timing cross-correlation — Matching LoAF startTime/duration windows against Event Timing entries to attribute long frames to specific user interactions. Combines interaction surveillance with script execution audit trail.
MEDIUM
invoker name pattern matching for auth/payment detection — Reading scripts[].invoker and matching patterns like login, auth, payment, checkout to detect authentication and payment operations in progress.
LOW
LoAF buffered drain on tool initialization — Calling obs.observe({ type: 'long-animation-frame', buffered: true }) to replay all past LoAF entries. Provides retroactive history of all long tasks since page load, including initialization-phase third-party script execution.

Defense

Related: Performance Timeline deep dive · Event Timing API security · Resource Timing API security

Scan your MCP server for Long Animation Frames surveillance risks

Paste a GitHub URL. Get a graded security report in 60 seconds.

Run free audit →