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 / SDK | Typical mark name patterns | Sensitive data in detail |
|---|---|---|
| React DevTools (Profiler) | ⚛ <Component> render, ⚛ <Component> commit | Component name, render duration, commit phase — reveals exact React component tree |
| Next.js App Router | next-router-change-start, next-hydration, next-render, next-client-cache-request | Route paths including dynamic segments (/users/[id]), hydration timing, cache hit/miss |
| Sentry Browser SDK | sentry-app-init, sentry-route-change, sentry-measure-* | DSN identifier, release version string, environment label, current route |
| OpenTelemetry / Datadog RUM | otel-span-start, dd-rum-perf-mark, trace span names | Trace IDs, span IDs, service name, sampling rate, user context |
| Angular (Zone.js) | Zone:<task>, Zone:invokeTask, NgZone:onStable | Zone names that encode application module boundaries |
| Vue DevTools / Nuxt | nuxt-link-prefetch, vue:mount, vue:update | Component names, prefetched route URLs |
| Application custom marks | auth-check-start, checkout-flow-start, user-loaded | userId, 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
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.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.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.performance.mark(name, { startTime: arbitraryTimestamp }) to inject fake historical events, corrupting A/B test attribution, APM dashboards, and session replay correlation.Defense
- Avoid sensitive data in mark detail objects. User IDs, session tokens, and PII should never be passed to
performance.mark()detail. Use opaque identifiers or omit user context entirely — APM can correlate server-side. - Permissions-Policy: unload and tool sandboxing — Running MCP tools in sandboxed iframes with restricted access to the main frame's performance timeline prevents buffer reads and mutations.
- Server-side APM where possible. Moving instrumentation to OpenTelemetry server-side spans eliminates the client-side mark surface. Client-only marks should contain only timing data, not application state.
- SkillAudit static scan — The SkillAudit scanner flags any
performance.getEntriesByType('mark'),PerformanceObserverwithtype: 'mark', orperformance.clearMarks()calls in a Claude skill, generating CRITICAL or HIGH findings that block installation in gated CI environments.