Security Guide

MCP server Priority Hints (fetchpriority) security — timing oracles, cross-origin resource detection, and resource ordering side channels

Priority Hints — the fetchpriority attribute introduced in Chrome 102 — let web pages influence the browser's internal resource scheduler, signaling which network requests should be initiated first. Accepted values are high, low, and auto, and the attribute works on <img>, <link>, <script>, and the fetch() API. While designed as a performance tool, fetchpriority creates attack surfaces: eager high-priority preloads generate measurable network timing that leaks cross-origin URL existence, font preloads combined with CORP errors expose cross-origin resource presence, and priority-induced request ordering creates observable side channels. MCP tool output that injects fetchpriority attributes can influence the browser's resource scheduling in ways that constitute timing-based information leaks — with no Permissions-Policy directive to block the feature entirely.

What fetchpriority does and where it applies

The fetchpriority attribute injects a hint into the browser's resource priority queue. The browser's resource scheduler manages a queue of pending network requests and assigns each an internal priority. fetchpriority="high" moves a resource to the front of that queue; fetchpriority="low" moves it to the back. The scheduler uses these hints — along with resource type, position in the document, and viewport visibility — to decide fetch ordering.

<!-- fetchpriority on img — tells browser to fetch this image before other images -->
<img src="/hero.webp" fetchpriority="high" alt="Hero image">

<!-- fetchpriority on link preload — triggers eager fetch for fonts or stylesheets -->
<link rel="preload" as="font" crossorigin href="/fonts/inter.woff2" fetchpriority="high">

<!-- fetchpriority on script — defers the script relative to others -->
<script src="/analytics.js" fetchpriority="low"></script>

<!-- fetchpriority in fetch() API — priority hint on programmatic requests -->
const response = await fetch('/api/critical-data', {
  priority: 'high'  // fetch() uses 'priority' not 'fetchpriority'
});

<!-- Cross-origin preload with fetchpriority — creates timing oracle -->
<link rel="preload"
      as="fetch"
      crossorigin="anonymous"
      fetchpriority="high"
      href="https://target.example/private/resource">

Attack surface 1: cross-origin URL existence timing oracle

When a page includes <link rel="preload" fetchpriority="high" href="https://target.example/page">, the browser immediately initiates a high-priority network request to that URL. The timing of when that request completes — or fails — leaks information about the target server:

An attacker-controlled MCP tool output injects a list of high-priority preload links for target URLs and measures, via PerformanceObserver, the response timing for each — creating a URL enumeration oracle against the target server.

// Attack: use PerformanceObserver to measure timing of high-priority preloads
// injected by MCP tool output — reveals which URLs exist on target server

// Attacker injects these preload links via MCP tool output:
// <link rel="preload" as="fetch" crossorigin fetchpriority="high"
//       href="https://target.example/user/alice/profile">
// <link rel="preload" as="fetch" crossorigin fetchpriority="high"
//       href="https://target.example/user/bob/profile">
// <link rel="preload" as="fetch" crossorigin fetchpriority="high"
//       href="https://target.example/admin/dashboard">

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.initiatorType === 'link' && entry.name.includes('target.example')) {
      const duration = entry.responseEnd - entry.startTime;
      const transferSize = entry.transferSize; // 0 for CORS-blocked, >0 if readable

      // Fast response + non-zero transfer = URL exists and content was served
      // Fast response + zero transfer = request was CORS-blocked but URL was hit
      // Slow response = URL exists but requires server-side computation
      // Very fast response (< 10ms) + zero transfer = URL does not exist (fast 404)

      console.log(`URL: ${entry.name} | Duration: ${duration}ms | Size: ${transferSize}`);
    }
  }
});
observer.observe({ type: 'resource', buffered: true });

CORS does not fully prevent the oracle. Even when cross-origin requests are blocked by CORS — returning a network error to the JavaScript context — the timing of the DNS lookup, TCP handshake, and TLS negotiation is measurable. A URL that exists on a reachable server has measurably different timing characteristics than a URL that returns a 404 before establishing a full connection. fetchpriority="high" ensures these requests are initiated immediately and with the highest scheduler priority, maximizing timing precision.

Attack surface 2: font preload with CORP error as resource existence detector

Font preloads with crossorigin and fetchpriority="high" generate both a network timing signal and an error event. When the browser attempts to load a font from a cross-origin URL, two outcomes are distinguishable:

// Attack: inject font preload via MCP tool output to detect cross-origin
// resource existence using CORP error event timing

// Injected by MCP tool output:
// <link id="probe" rel="preload" as="font" crossorigin="anonymous"
//       fetchpriority="high"
//       href="https://target.example/private/confidential-doc.pdf">

const probeLink = document.getElementById('probe');
const startTime = performance.now();

probeLink.addEventListener('error', () => {
  const elapsed = performance.now() - startTime;

  // Timing analysis:
  // elapsed > 100ms: server responded (resource exists, blocked by CORP)
  // elapsed < 20ms: resource doesn't exist (fast 404 before full connection)
  // elapsed > 500ms: server-side processing (resource exists, requires computation)

  console.log(`Font probe result: ${elapsed}ms — resource likely ${elapsed > 100 ? 'EXISTS' : 'MISSING'}`);
});

Attack surface 3: resource ordering side channel

When multiple resources compete for bandwidth and scheduler slots, fetchpriority="high" resources pre-empt lower-priority requests. An observer script on the same page — or in a same-origin MCP tool output frame — can measure which of several competing resources arrived first, revealing the browser's priority decisions. This creates an indirect signal about which resources the page author (or server) considered most important, potentially leaking information about business logic embedded in priority assignments.

// Scenario: MCP renderer page loads several resources with different priorities
// An injected script in tool output measures arrival order to infer priorities

const resourceTimings = {};
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    resourceTimings[entry.name] = {
      start: entry.startTime,
      end: entry.responseEnd,
      // Resources that complete first were fetched with higher priority
      // or were already cached — both reveal information about their importance
    };
  }
  // Sort by completion time — first to complete had highest priority
  const ordered = Object.entries(resourceTimings)
    .sort(([, a], [, b]) => a.end - b.end);
  console.log('Resource arrival order (highest priority first):', ordered.map(([url]) => url));
});
observer.observe({ type: 'resource', buffered: true });

No Permissions-Policy directive for fetchpriority

Unlike many sensitive browser features — such as camera, microphone, geolocation, and idle detection — there is no Permissions-Policy directive that disables or restricts fetchpriority. Site owners cannot use a response header to prevent embedded MCP tool output from injecting high-priority preload links or influencing the resource scheduler. The only effective defenses are architectural: strict CSP that blocks cross-origin preloads, and MCP renderer isolation that prevents tool output from sharing a JavaScript execution context with the host page.

FeaturePermissions-Policy directivefetchpriority
Geolocation geolocation=() Fully disabled in embedded contexts
Camera camera=() Fully disabled in embedded contexts
Idle Detection idle-detection=() Fully disabled in embedded contexts
fetchpriority attribute None — no Permissions-Policy directive exists Cannot be disabled via Permissions-Policy; must use CSP or isolation

Defenses

DefenseBlocks fetchpriority attacks?Notes
Strict CSP: default-src 'self' Yes — blocks cross-origin preload link elements Prevents MCP tool output from injecting preload links to external URLs; most effective for cross-origin URL oracle attacks
CSP: prefetch-src 'none'; preload-src 'self' Yes — specifically restricts preload targets More targeted than default-src; allows same-origin preloads while blocking cross-origin timing oracles
MCP renderer isolation (cross-origin iframe with sandbox) Yes — tool output executes in a separate null-origin context Prevents tool output from observing the host page's resource timing via PerformanceObserver; also blocks same-page ordering side channels
Cross-Origin-Resource-Policy: same-origin on all private resources Partial — reduces information leaked via CORP error oracle Makes CORP blocking consistent across all resources; reduces but does not eliminate timing differences
Static analysis of MCP tool output templates for fetchpriority injection Detects — scan for fetchpriority="high" and rel="preload" with cross-origin href SkillAudit performs this check

Findings SkillAudit reports

High MCP tool output injects <link rel="preload" fetchpriority="high" href="https://[cross-origin]"> elements — creates timing oracle for cross-origin URL existence detection; no CSP preload restriction in place
High Tool output contains <link rel="preload" as="font" crossorigin fetchpriority="high" href="..."> pointing to cross-origin URLs — font CORP error + timing reveals whether the target resource exists
Medium MCP renderer shares a JavaScript execution context with the host page — tool output can observe host page resource timings via PerformanceObserver, leaking resource ordering information determined by fetchpriority
Medium No default-src 'self' or preload-specific CSP directive — tool output can inject arbitrary preload links to external origins to conduct timing-based URL enumeration
Low Private resources served without Cross-Origin-Resource-Policy header — CORP error timing oracle is more reliable because inconsistent CORP responses produce distinguishable timing signals

Related guides: Speculation Rules API security, Prerender security, MCP server CSP configuration.

Get a graded audit. Paste your MCP server's GitHub URL at skillaudit.dev for a report covering Priority Hints injection risks, CSP preload policy analysis, renderer isolation assessment, and your full browser timing attack surface in 60 seconds.