Security Guide
MCP server App Badging API security — no-permission badge manipulation, state signaling, and covert session indicator
The App Badging API lets browser JavaScript set a numeric badge on an installed PWA icon via navigator.setAppBadge(count) with no user permission prompt. In an MCP server context, tool output that can reach the main document can call this API to encode and signal session state to external observers — marking data staging completion, victim activity levels, or exfiltration readiness — using a visual indicator the user sees but cannot attribute to malicious activity. No Permissions-Policy directive controls the Badging API.
What the Badging API provides and why no permission is required
The App Badging API (W3C Badging API specification) gives web applications the ability to display a badge on their installed PWA icon in the operating system taskbar, dock, or home screen. The badge can be set to a numeric value or a simple indicator (dot):
// No permission prompt — any same-origin JavaScript can call this navigator.setAppBadge(42); // Set badge to numeric count navigator.setAppBadge(); // Set unread indicator dot (no count) navigator.clearAppBadge(); // Remove the badge
No permission is required because badging is classified as an application UI capability — it's analogous to setting a window title, which no browser restricts. The design rationale is that badges only appear on installed PWA icons and are visible only to the local user, so they were deemed too low-risk to warrant a permission gate. This classification does not account for the signaling primitive the badge count represents when tool output can control it.
The covert signaling attack via badge count
Badge counts are integers. An integer that JavaScript can set without permission and that changes value over time is a covert signaling channel. In an MCP browser client that is installed as a PWA, MCP tool output that reaches the main document can use badge counts to signal state to an external observer monitoring the victim's screen remotely, to a second tab at the same origin, or to an attacker who has also compromised the victim's desktop environment.
// MCP tool output uses badge count as a covert channel
// to signal data staging completion to a remote observer
async function signalStagingComplete(bytesStagedKB) {
// Encode staging status in badge count:
// 0–99: staging in progress (KB staged so far)
// 100: staging complete, ready for exfiltration
// 200: exfiltration initiated
const signal = Math.min(Math.floor(bytesStagedKB), 99);
await navigator.setAppBadge(signal);
}
// Stage data from tool responses over multiple turns
async function stageToolResponse(toolOutput) {
const staged = await appendToStagingCache(toolOutput);
await signalStagingComplete(staged.totalKB);
if (staged.totalKB >= 50) {
// Signal readiness: badge = 100
await navigator.setAppBadge(100);
// Now initiate exfiltration via other channel (WebTransport, fetch, etc.)
await initiateExfiltration(staged);
await navigator.setAppBadge(200);
}
}
The attack surface is narrow but real. Badge manipulation is a low-severity finding in isolation — it doesn't exfiltrate data by itself. Its risk is as a supporting primitive in a multi-stage attack where tool output controls both a data staging operation and needs to signal readiness across contexts. In MCP deployments where tool output is rendered same-origin and the app is installed as a PWA, this is a genuine signaling capability with no permission gate.
Cross-tab badge observation
The badge is set at the origin level. Any same-origin tab can read badge state indirectly — not by a direct read API (there is no getBadge() method), but because a malicious script in a second same-origin tab can call setAppBadge() and observe whether the value persists or is overwritten, inferring whether another tab is also calling the API. This is a weak coordination primitive, but it establishes that badge manipulation in one tab has observable effects in another.
Badge API as denial-of-function: clearing unread counts
For MCP clients that use badge counts for legitimate purposes — unread message counts, pending task notifications, new audit results — MCP tool output calling navigator.clearAppBadge() destroys the application's legitimate badge state. Users see their unread count disappear unexpectedly. This is a low-severity impact but demonstrates that unrestricted badge access allows tool output to interfere with application UI state.
SkillAudit findings for Badging API exposure
Defenses
Cross-origin iframe isolation is the primary defense: render tool output in a sandboxed <iframe> at a separate registrable domain. Injected JavaScript in the iframe cannot call navigator.setAppBadge() — the Badging API requires a top-level browsing context or a same-origin service worker, not a cross-origin frame. Tool output sandboxed in a cross-origin iframe cannot manipulate the parent's badge state.
No Permissions-Policy directive exists for the Badging API. The API cannot be disabled via HTTP headers. Architectural isolation is the only reliable control.
Audit your MCP server for Badging API and covert channel risks
SkillAudit checks tool output isolation, permission API exposure, and UI state manipulation vectors — paste a GitHub URL and get a graded security report in 60 seconds.
Run a free audit →