MCP server HTML canvas element consent security

The HTML <canvas> element renders graphics and text imperatively through a JavaScript drawing API. Unlike <p> or <div> elements, text painted on a canvas with ctx.fillText() creates no DOM text nodes — it exists only as pixel data in the canvas bitmap. This places canvas-rendered consent text entirely outside the reach of document.querySelectorAll(), element.innerText, ARIA accessibility tree inspection, and every other DOM-based auditing method. MCP servers exploit this DOM blind spot through four canvas consent attacks: rendering consent text exclusively on canvas to evade DOM auditors, using canvas fill styles matching the page background to make text invisible while technically "present", overlaying a blank canvas on top of a real consent <div> to cover it, and using OffscreenCanvas to render in a context that never attaches to any visible surface.

Finding SA-CVS-001: consent text drawn with ctx.fillText() — invisible to DOM auditors, querySelector, and accessibility tree

CriticalWhen consent text is drawn on a <canvas> element using ctx.fillText("By clicking Install, you grant credential access…", x, y), the consent content exists as rendered pixels in the canvas bitmap. No <p>, <span>, or text node is created in the DOM. Every standard DOM-based auditing technique — document.querySelectorAll('[class*="consent"]'), document.body.innerText, document.evaluate() XPath traversal, screen reader accessibility tree inspection — returns no results for the consent text. The canvas element itself is present in the DOM and may have an aria-label attribute to claim it shows consent, but the actual text content is only verifiable by reading the canvas pixel buffer with ctx.getImageData() or by executing the JavaScript drawing code path and capturing the rendered output. Visual-only inspection of a screenshot will show the consent text; DOM inspection will not find it.
<!-- ATTACK: consent text exists only as canvas pixels, not as DOM text -->
<canvas id="consentCanvas" width="400" height="120"
        aria-label="Consent disclosure" role="img"></canvas>

<button id="installBtn">Install MCP Server</button>

<script>
const canvas = document.getElementById('consentCanvas');
const ctx = canvas.getContext('2d');

// Draw consent text — no DOM nodes created
ctx.font = '13px sans-serif';
ctx.fillStyle = '#6b7280';
ctx.fillText('By clicking Install, you grant this MCP server access to', 10, 30);
ctx.fillText('your credentials, API keys, and private repositories.', 10, 50);
ctx.fillText('This authorization is permanent until manually revoked.', 10, 70);

// Mark consent as shown — no DOM text nodes were created
consentDisplayed = true;

document.getElementById('installBtn').addEventListener('click', () => {
  if (consentDisplayed) recordConsent(); // Always true
});

// Auditor check: document.body.innerText.includes('credentials') → false
// Auditor check: document.querySelectorAll('.consent') → []
// Visual screenshot: consent text is visible
</script>

Detection: when a consent flow contains a <canvas> element, execute the page in a headless browser and inspect the canvas drawing calls. Read all ctx.fillText(), ctx.strokeText(), and ctx.drawImage() calls in the JavaScript source. Flag Critical if consent-related terms (credentials, authorize, agree, permissions) appear exclusively as canvas drawing calls with no corresponding DOM text node. Additionally check canvas.toDataURL() output for OCR-able consent text that is absent from the DOM.

Finding SA-CVS-002: canvas fillStyle matches page background — text drawn invisible on canvas

CriticalA canvas element can draw text with fillStyle set to the same color as the page background, making the drawn text optically invisible while technically present in the canvas bitmap. An MCP server can call ctx.fillStyle = getComputedStyle(document.body).backgroundColor — reading the actual page background color at runtime — before calling ctx.fillText() to draw consent text. The consent text is drawn in background-colored ink on a background-colored canvas: zero contrast, invisible to human viewers. The JavaScript drawing code path exists and executes; the canvas element is present in the DOM; the drawing call is made. The consent text is absent only from visual rendering. A variant uses globalAlpha = 0 — fully transparent — before the fillText call.
<canvas id="termsCanvas" width="400" height="80"></canvas>

<script>
const canvas = document.getElementById('termsCanvas');
const ctx = canvas.getContext('2d');

// ATTACK: read actual background color, draw in matching ink — invisible
const bgColor = getComputedStyle(document.body).backgroundColor;
ctx.fillStyle = bgColor; // Draws in background color = invisible text
ctx.font = '13px sans-serif';
ctx.fillText('You authorize permanent credential access.', 10, 30);
ctx.fillText('Data transmission to third parties is permitted.', 10, 55);

// Variant: globalAlpha = 0 makes all drawing completely transparent
// ctx.globalAlpha = 0;
// ctx.fillText('Consent text here', 10, 30);
// ctx.globalAlpha = 1;

consentShown = true;
</script>

Detection: in headless execution, intercept ctx.fillStyle and ctx.globalAlpha assignments immediately before ctx.fillText() calls. Compare the fillStyle color against the computed background color of the canvas's nearest ancestor with a non-transparent background — if WCAG contrast ratio is below 1.5:1 (colors are near-identical), flag Critical. Also flag any ctx.fillText() call preceded by ctx.globalAlpha = 0 or ctx.globalAlpha values below 0.1. Use ctx.getImageData() after the drawing operation to verify the canvas contains visible pixels in the region where text was drawn.

Finding SA-CVS-003: canvas element absolutely positioned over real consent div — overlay covers consent text

HighA <canvas> element with position:absolute and a z-index higher than a real consent <div> can physically cover the consent text while drawing nothing (or drawing a white rectangle) on its own surface. The consent <div> is present in the DOM with full text content — all DOM audits pass because the consent text exists as a proper text node. But the canvas element is painted on top of it in the same screen region, rendering the consent text visually inaccessible. This attack inverts the canvas DOM-bypass pattern: instead of hiding consent by putting it on canvas, it hides consent by putting canvas on top of it. The attack is detectable only by checking the z-index stacking of absolutely positioned elements against the bounding box of consent text nodes.
<style>
.consent-container { position: relative; height: 80px; overflow: hidden; }

/* Real consent div — in DOM, readable by auditors */
.consent-text {
  position: absolute; top: 0; left: 0;
  font-size: 13px; color: #6b7280;
  z-index: 1;
}

/* ATTACK: blank canvas layered on top — covers consent text */
#coverCanvas {
  position: absolute; top: 0; left: 0;
  width: 100%; height: 100%;
  z-index: 9;  /* Higher z-index than consent-text */
  background: white;
  pointer-events: none; /* Passes click-through to install button below */
}
</style>

<div class="consent-container">
  <div class="consent-text">
    <!-- DOM auditors find this text — it exists in the document -->
    By clicking Install, you grant credential access and authorize
    data transmission to third-party services.
  </div>
  <!-- ATTACK: canvas covers consent text in rendered output -->
  <canvas id="coverCanvas"></canvas>
</div>

Detection: compute the bounding rectangle of all DOM text nodes containing consent content. Check for any <canvas> elements whose bounding rectangle overlaps the consent text bounding rectangle by more than 50%. If the canvas has a higher computed z-index than the consent text's containing element and the canvas background color matches the page background (or the canvas is simply opaque white), flag High. Also check pointer-events:none on the canvas — this is a common pattern to allow click-through while maintaining visual coverage, and is not a benign pattern when combined with consent overlap.

Finding SA-CVS-004: OffscreenCanvas renders consent in detached context — never attached to visible surface

HighThe OffscreenCanvas API creates a canvas context that is not attached to any DOM element. Rendering to an OffscreenCanvas produces pixel output that exists only in memory — it can be transferred to a Blob or an ImageBitmap, but unless the result is explicitly drawn into a visible <canvas> element, it is never painted to any screen surface. An MCP server that draws consent text to an OffscreenCanvas and then never transfers the result to a visible canvas has technically executed the drawing code — an auditor checking for canvas usage will find a drawing call with consent text — but the user never sees any consent UI. OffscreenCanvas also runs in Web Workers, making the drawing call happen entirely off the main thread with no UI impact.
<script>
// ATTACK: consent drawn to OffscreenCanvas — never appears on screen
const offscreen = new OffscreenCanvas(400, 100);
const ctx = offscreen.getContext('2d');
ctx.font = '13px sans-serif';
ctx.fillStyle = '#374151';
ctx.fillText('Authorization granted for credential access.', 10, 30);
ctx.fillText('You agree to data processing and third-party sharing.', 10, 55);

// Consent text was "drawn" but never transferred to a visible canvas
// offscreen.convertToBlob() or ctx.transferToImageBitmap() could export it
// — but neither is called. The rendered pixels go nowhere.

consentRendered = true; // Flag set — consent "rendered" (in memory only)

// Web Worker variant — runs entirely off-thread:
// const worker = new Worker('consent-worker.js');
// worker.postMessage({ type: 'drawConsent' });
// Inside worker: const ctx = new OffscreenCanvas(400,100).getContext('2d');
//                ctx.fillText('Consent text...', 10, 30); // Never displayed
</script>

Detection: scan JavaScript source for new OffscreenCanvas( or canvas.transferControlToOffscreen() calls. Identify any fillText() calls on the resulting context. If consent-related text strings appear in fillText() arguments on an OffscreenCanvas context, verify that the result is transferred back to a visible on-screen <canvas> element via drawImage(imageBitmap) on a visible canvas context. If no such transfer occurs — or the transfer is conditional on a flag that is never set — flag High. Web Worker canvas operations require scanning worker source files for the same pattern.

Canvas consent attack auditability comparison

Finding Attack DOM auditor Screenshot audit JS execution audit
SA-CVS-001 fillText — no DOM text node Misses Catches (text visible in screenshot) Catches (intercept fillText)
SA-CVS-002 fillStyle = backgroundColor Misses Misses (text invisible) Catches (contrast ratio check)
SA-CVS-003 Canvas overlay over consent div Passes (div present) Catches (div obscured) Catches (z-index overlap check)
SA-CVS-004 OffscreenCanvas — off-screen only Misses Misses (never visible) Catches (no visible transfer)

SkillAudit executes MCP server consent flows in a headless browser with instrumented canvas APIs — intercepting fillText(), fillStyle, and OffscreenCanvas calls to detect consent text that bypasses DOM inspection. Run a free audit to check whether your MCP server's consent text is actually visible to users.