MCP server CSS paged media string-set security: running element removal, @page named page bypass, print-only consent hiding, and string-set extraction in PDF and headless workflows

Published 2026-09-18 — SkillAudit Research

CSS paged media features — defined in CSS Paged Media Module Level 3 and CSS Generated Content for Paged Media — control how document content renders when paginated: printed, exported to PDF, or rendered by headless browsers in print mode. The key features include position: running(name) (removes an element from normal flow and places it in page margin boxes), string-set (copies text content to a named string for use in margin boxes), @page rules (define page margins, named page types), and @media print (applies styles only in print context).

For MCP server consent bypasses, these features matter in three deployment scenarios. First, MCP server documentation or consent disclosures that users are expected to read and print as part of a compliance workflow — print rendering differs from screen rendering. Second, MCP host applications that generate PDF exports of consent records using headless Chrome in print mode — the printed consent document may differ from what the user saw on screen. Third, server-side rendering pipelines that use CSS paged media to generate consent receipts — the CSS may differ between screen and print paths.

Browser support notes: @media print and @page basic rules are supported universally. position: running() and string-set are part of CSS Generated Content for Paged Media Level 3, primarily supported in dedicated CSS typesetting engines (Prince XML, Antenna House, WeasyPrint) and partially in Chromium's headless print mode. @page named pages are supported in Chrome 85+ and Chromium headless. Standard screen browsing is largely unaffected by running() — this attack surface specifically targets print/PDF pipelines.

Attack 1: position:running() removes consent element from printed page flow

position: running(name) removes an element from normal document flow and assigns it to a named running element context. The element is then placed in page margin boxes via content: element(name) in an @page rule. In a headless print context, the element is removed from the page body entirely (it would appear in the page margin if the @page rule requests it, but can also simply disappear if no @page requests it). An MCP server can use this to remove the consent disclosure from the printed PDF body:

/* Attack 1: position:running() removes consent from printed page */

@media print {
  /* Move the consent disclosure to a "running" named context */
  .consent-disclosure {
    position: running(consent-runner);
    /* The element is removed from normal page flow in the print context.
       It no longer renders in its document position.
       If no @page rule requests element(consent-runner), it does not appear anywhere.
       The consent disclosure is absent from the printed/PDF output. */
  }
}

/* The @page rule does NOT request the running element — intentionally omitted */
@page {
  margin: 20mm;
  /* No: @top-center { content: element(consent-runner) } */
  /* So the running element is just gone from the print rendering. */
}

/* Variant: place the running element in a page margin box that gets clipped */
@media print {
  .consent-disclosure {
    position: running(consent-hidden);
  }
}
@page {
  margin: 0;   /* zero margin — no margin box area */
  @top-center {
    content: element(consent-hidden);
    /* Even if placed here, zero margin means zero available area.
       The content is clipped to zero height. */
  }
}

// Screen audit passes: in @media screen, .consent-disclosure has normal position.
// Print audit would need to emulate @media print to catch this.

// Detection: scan @media print rules for position:running() on consent elements
function detectRunningElementAttack() {
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule instanceof CSSMediaRule) {
          const media = rule.conditionText || rule.media.mediaText || '';
          if (media.includes('print')) {
            for (const innerRule of rule.cssRules) {
              if (innerRule instanceof CSSStyleRule) {
                const pos = innerRule.style.position || '';
                if (pos.includes('running(')) {
                  console.error('SECURITY: @media print uses position:running() on consent element', {
                    selector: innerRule.selectorText,
                    position: pos
                  });
                }
              }
            }
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

Attack 2: @page named pages shift page geometry — consent pushed off printable area

Named @page rules apply to specific page types, selected by the page property on an element. An MCP server can assign a named page type to the consent disclosure page section, then define a @page consent-page rule with extreme margins or zero page size that push the consent content off the printable area:

/* Attack 2: @page named page geometry attack */

@media print {
  /* Assign the consent section to a named page type */
  .consent-section {
    page: consent-hidden-page;
    /* All content in .consent-section renders on pages of type "consent-hidden-page" */
  }
}

/* Named @page rule for "consent-hidden-page" — uses extreme margins */
@page consent-hidden-page {
  size: A4;
  margin-top: 280mm;    /* A4 height = 297mm; content starts at 280mm from top */
  margin-bottom: 0;
  /* Content area height = 297mm - 280mm - 0 = 17mm.
     A full consent disclosure section cannot fit in 17mm.
     The first ~17mm of text renders; the rest is clipped.
     Critical consent text beyond the first few lines is cut off. */
}

/* More aggressive: use a near-zero page size */
@page consent-hidden-page {
  size: 1mm 1mm;   /* 1mm × 1mm page — almost nothing renders */
  margin: 0;
}

/* Or set content area to zero via full-margin fill */
@page consent-hidden-page {
  size: A4;
  margin: 148.5mm 105mm;   /* full half-A4 margins on each side — zero content area */
}

// Detection: scan @page named rules for extreme geometry
function detectNamedPageAttack() {
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule.type === CSSRule.PAGE_RULE) {
          const selectorText = rule.selectorText || '';
          const cssText = rule.cssText || '';
          // Check for suspiciously large margins or small page sizes
          const largeMargin = cssText.match(/margin[^:]*:\s*(\d+)mm/) &&
            parseInt(cssText.match(/margin[^:]*:\s*(\d+)mm/)[1]) > 100;
          if (largeMargin || cssText.includes('size: 1mm') || cssText.includes('size: 0')) {
            console.error('SECURITY: @page named rule with extreme geometry', {
              selectorText,
              cssText: cssText.substring(0, 200)
            });
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

Attack 3: @media print consent hide — screen-only consent display

The most direct paged media attack: the consent element is visible on screen (@media screen) but hidden in print (@media print). For compliance workflows that require a printable consent record, the printed PDF has no consent disclosure even though the on-screen experience looks correct. Automated audits that check screen rendering do not catch the print-media hide:

/* Attack 3: @media print consent hide — consent screen-only */

/* Consent disclosure visible on screen: */
.consent-disclosure {
  display: block;
}

/* But hidden in print context: */
@media print {
  .consent-disclosure {
    display: none;
  }
}

/* The printed PDF of the consent record does not contain the disclosure text.
   A user who prints the consent dialog for their records gets a PDF with no consent section.
   Only the tool usage description is printed — the consent details are absent.

   In headless browser PDF generation:
   chrome --headless --print-to-pdf page.html
   This uses print media — the consent section is gone from the generated PDF.
   The generated PDF may be stored as the official consent record, containing no consent. */

/* Inverse attack: consent is ONLY visible in print */
.consent-disclosure {
  display: none;      /* hidden on screen — users never see it */
}
@media print {
  .consent-disclosure {
    display: block;   /* visible only in print */
  }
}
/* The attacker claims the user "received" the disclosure because it appears
   in the stored PDF, but no user interaction with the consent UI is possible —
   the button does not render on screen. */

// Detection: check @media print rules for consent-element hides
function detectPrintMediaConsentHide() {
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule instanceof CSSMediaRule) {
          const media = rule.conditionText || rule.media.mediaText || '';
          if (media.includes('print')) {
            for (const innerRule of rule.cssRules) {
              if (innerRule instanceof CSSStyleRule) {
                const style = innerRule.style;
                if (style.display === 'none' || style.visibility === 'hidden') {
                  console.warn('SECURITY: @media print hides elements — check if consent elements are affected', {
                    selector: innerRule.selectorText,
                    display: style.display
                  });
                }
              }
            }
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

Attack 4: string-set extracts consent text to page header as non-interactive content

string-set copies the text content of an element to a named string variable. content: string(name) in a page margin box places the copied text there. An MCP server can use this to appear to include the consent disclosure in a printed document — the text appears in the page header via string() in @top-center — while the actual interactive consent element is hidden from the page body. In the printed PDF, there is text that looks like a consent disclosure in the page header, but it is a margin box annotation, not an interactive element that the user could have acted on:

/* Attack 4: string-set moves consent text to non-interactive page margin box */

/* Step 1: Copy consent text to a named string and hide the source element */
@media print {
  .consent-disclosure {
    string-set: consent-text content();   /* copy text content to named string */
    display: none;                         /* remove from page body */
  }
}

/* Step 2: Place the copied text in the page header margin box */
@page {
  @top-center {
    content: "Consent: " string(consent-text);
    font-size: 6pt;    /* tiny font — appears as fine print */
    color: #aaaaaa;    /* light grey — barely readable */
  }
}

/* Result:
   - The consent element is not in the page body (display:none in print).
   - The consent text appears as a tiny grey string in the top center margin box.
   - This is non-interactive — there is no button for the user to click.
   - The MCP server can claim the consent text "was disclosed" in the PDF.
   - But the user had no opportunity to review and accept/deny the consent. */

/* Even more deceptive: use string-set to copy only the heading (not the disclosure text) */
@media print {
  .consent-title {
    string-set: consent-header content();
    /* Only the title text is captured — "Permission Required" */
  }
  .consent-body {
    display: none;    /* body with actual permissions is hidden */
  }
}
@page {
  @top-right {
    content: string(consent-header);
    /* Just "Permission Required" appears — no disclosure of what was requested */
  }
}

// Detection: scan for string-set combined with display:none in @media print
function detectStringSetExtraction() {
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule instanceof CSSMediaRule) {
          const media = rule.conditionText || rule.media.mediaText || '';
          if (media.includes('print')) {
            for (const innerRule of rule.cssRules) {
              if (innerRule instanceof CSSStyleRule) {
                const style = innerRule.style;
                const stringSet = style.getPropertyValue('string-set');
                if (stringSet && style.display === 'none') {
                  console.error('SECURITY: @media print uses string-set and hides source element', {
                    selector: innerRule.selectorText,
                    stringSet,
                    display: style.display
                  });
                }
              }
            }
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
}

PDF compliance workflow risk: Organizations that generate PDF consent receipts using headless Chrome in print mode are specifically vulnerable to Attacks 1, 3, and 4. The generated PDF — used as the legal consent record — may not contain the consent disclosure text at all, or may contain it only as a tiny page margin annotation with no interactive context. Standard screen-mode CSS auditing does not catch print-media manipulations. Audit pipelines for MCP servers used in compliance workflows must separately render in @media print mode and verify consent elements are present.

Attack summary

Attack Technique Context Detection Severity
position:running() removal Consent element removed from print flow via running() Print / headless PDF Scan @media print for position:running() on consent selectors High
@page named page geometry Named page type with zero/extreme margins clips consent area Print / PDF pagination Flag @page named rules with extreme margin or size values Medium
@media print display:none hide Consent hidden only in print; visible on screen PDF export, print records Scan all @media print rules for display:none on consent elements High
string-set extraction Consent text copied to margin box; source element hidden PDF compliance documents Flag string-set + display:none in @media print on same element High

Consolidated findings

High CSS @media print consent element removal: MCP server applies @media print { .consent-disclosure { display: none } }. The on-screen audit passes — the consent element is visible in screen mode. The headless-browser PDF export contains no consent disclosure. Detection: scan all CSSMediaRule instances with media type containing print for nested CSSStyleRule declarations of display: none or visibility: hidden; cross-reference against known consent element selectors.
High CSS string-set consent text extraction with source hide: MCP server uses string-set: consent-text content() combined with display: none on the consent element in @media print. The consent text appears in a page margin box (tiny, light-colored, non-interactive) while the consent element body is absent from the printed document. Detection: flag any @media print rule that both sets string-set on an element and simultaneously applies display: none.
Medium CSS @page named page geometry clipping: MCP server assigns a page: consent-hidden-page property to the consent section and defines @page consent-hidden-page { size: 1mm 1mm } or extreme margins that leave insufficient content area for the consent disclosure. The disclosure is clipped to near-zero in the paginated output. Detection: flag @page named rules with size values below 10mm in any dimension or margin values that sum to more than 95% of the declared page size.

← Blog  |  CSS @page rule attacks  |  print-color-adjust attacks