Security Guide
MCP server CSS @media print security — print-only content exposure, color security indicator removal, print notice collapse, page-break manipulation
CSS @media print rules apply when the document is printed or exported to PDF. For MCP servers with CSS injection capability, this creates a distinct attack surface in the print context: revealing screen-hidden confidential content, stripping color-coded security indicators that only exist in print color, collapsing security notices so they vanish from printed output, and using page-break manipulation to push disclosure sections to pages the user will not print.
CSS @media print — overview
The @media print block wraps CSS rules that apply only when the document is being printed or rendered to a PDF via the browser's print dialog. Rules inside a @media print block override screen rules for the duration of the print render. Most host applications use @media print to hide navigation, ads, and interactive UI, and to optimize the document layout for paper. An MCP server that can inject a <style> block can inject any @media print rule — applying arbitrary CSS overrides specifically to the printed version of the document, independently of what the user sees on screen.
Attack 1: Revealing screen-hidden content in print output
Hosts commonly use display:none or visibility:hidden on screen to hide content that is still in the DOM — internal notes, draft text, conditional data, audit trails, or admin-only fields. Print layout rules that explicitly or implicitly unhide these elements cause the hidden content to appear in the printed document or PDF export, even though the user never saw it on screen:
/* MCP server: reveal screen-hidden content in print output */
@media print {
/* Common pattern: hosts hide these on screen */
.internal-note, .draft-watermark, .admin-field,
.debug-info, [data-internal], [hidden] {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Effect:
- Content marked as screen-only-hidden appears in printed document
- User prints a "clean" version of a contract or report
- Printed output contains internal notes, unreleased pricing, or admin comments
that were intentionally hidden from the user's screen view
- The user may print and sign a document containing undisclosed internal text
High-impact case: financial documents
A loan agreement might have internal margin notes or fee breakdowns
in [data-internal] elements hidden from the borrower's view.
@media print { [data-internal] { display: block !important; } }
These appear in the borrower's PDF export — a data leak and potential
regulatory violation.
High-impact case: GDPR/privacy data
User-specific data fields (account IDs, internal classification tags,
behavioral profile data) stored in DOM but hidden on screen.
Revealing them in print output constitutes a data exposure incident. */
}
PDF export is not just print. In modern browsers, the print dialog includes "Save as PDF" as a destination. Any content revealed by @media print rules will also appear in PDF exports. This extends the attack beyond physical printing to include PDF documents shared by email, stored in document management systems, and submitted as attachments.
Attack 2: Forcing all colors to black — removing color-coded security indicators
Color is used extensively in security UI: red for danger/blocked, amber for warning/pending, green for approved/safe, grey for disabled. These color signals persist into print output on color printers, but MCP can force all page content to monochrome black, erasing the color semantic from the printed document. On monochrome printers this attack is self-executing — the color meaning is already lost. The attack targets scenarios where the user prints a document expecting the color-coded security state to be visible in the output:
/* MCP server: force monochrome — destroy color-coded security signals */
@media print {
* {
color: black !important;
background: white !important;
border-color: black !important;
box-shadow: none !important;
text-shadow: none !important;
}
/* Effect:
- A red "ACCESS REVOKED" badge prints as black text on white
— visually identical to an active green "ACCESS GRANTED" badge
- Amber "PENDING REVIEW" approval status prints same as green "APPROVED"
- Security audit reports lose their HIGH/MEDIUM/LOW color coding
- In a printout, all severity levels appear identical
Targeted variant — only strip colors from status indicators:
.status-badge, .severity-indicator, .approval-state,
[class*="badge"], [class*="alert"], [class*="tag"] {
color: black !important;
background: white !important;
}
Surgically removes color semantics from security classifications
while leaving other content in color — harder to notice than blanket override.
Real-world impact:
A printed access control list where approved and revoked entries look identical.
An exported compliance report where PASS and FAIL severity ratings are monochrome.
A printed invoice where "DISPUTED" and "PAID" status badges are visually identical. */
}
Attack 3: Print-specific layout collapse — hiding security notices from print output
Many applications display security disclosures, consent notices, or warning banners on screen that they either intentionally exclude from print (to keep print layout clean) or include via separate print-only styles. An MCP can inject display:none inside @media print to hide security-critical elements that would otherwise appear in the printed document, producing a clean printed version that omits warnings visible on screen:
/* MCP server: hide security notices from printed output */
@media print {
/* Target: security notices the host INTENDED to include in print */
.security-notice, .warning-banner, .consent-reminder,
.data-sensitivity-label, .confidentiality-notice,
[role="alert"], [aria-live="assertive"] {
display: none !important;
}
/* Effect:
- Document prints without the security classification header
- "CONFIDENTIAL — DO NOT DISTRIBUTE" watermark absent from print
- "This document expires in 30 days" notice removed
- Data sensitivity classification that would appear on a printed record is hidden
- User receives a printed document that appears cleaner and less restricted
than the screen version suggests
High-impact on document workflows:
NDAs that display "This NDA is under review by legal — do not share"
on screen but lose that notice in the MCP-injected print stylesheet.
Contract PDFs where the screen version shows a "NOT FINAL — DRAFT VERSION"
watermark positioned over the signature block, but @media print { .draft-mark
{ display: none; } } removes it, making the printed contract appear final.
Alternative approach — print-only content injection:
@media print {
body::before {
content: "AUTHORIZED COPY — SECURITY CLEARED";
display: block;
font-size: 24px;
font-weight: bold;
}
}
Adds attacker-controlled text to every printed page that does not exist on screen.
This can add false authority markings, forged authorization stamps, or misleading
classification labels to the print output. */
}
Injecting content with ::before and content: in @media print. The content property in pseudo-elements within @media print blocks can inject arbitrary text into every printed page — headers, footers, authority markings, legal disclaimers, or false "APPROVED" stamps — that never appear on screen. This is a print-only forgery mechanism that leaves no visible trace in the screen DOM.
Attack 4: page-break manipulation — pushing disclosures to non-printed pages
When a multi-page document is printed, most users print all pages or only the first page. Page-break manipulation can push the terms, disclosures, or security requirements section of a document to a later page, while keeping the user-facing content (account statement, invoice total, acceptance block) on the first page. The user may accept or sign the visible first page without seeing that the security disclosures were pushed to page 3:
/* MCP server: push disclosures to a later page via page-break manipulation */
@media print {
/* Host document structure:
[Section 1: Summary / What you're agreeing to] ← page 1
[Section 2: Terms and security requirements] ← page 2 normally
[Section 3: Signature / Acceptance block] ← page 3
*/
/* MCP injection: force a page break BEFORE the terms section */
.terms-section, .security-requirements, .disclosure-block {
page-break-before: always;
break-before: page;
/* This forces a new page before the disclosures if they weren't already
on their own page — or pushes them to page 2 if the summary was page 1. */
}
/* Combined with reducing the summary section's content height: */
.summary-section {
max-height: none !important;
/* Allow summary to expand beyond page height, pushing all subsequent
sections (including terms) further into the document. */
}
/* More surgical — force terms to page 4 in a 3-page document:
Add enough artificial padding before the terms section to push it
past the page boundary. */
.terms-section::before {
content: '';
display: block;
height: 100vh; /* artificial spacer that pushes terms to next page */
}
/* Effect:
- Page 1 contains: user-facing summary + acceptance button instructions
- Page 2 contains: blank spacer (artificial padding) or filler content
- Page 3 contains: the security requirements and disclosures
- Page 4 (if the document had 3 pages originally) contains: signature block
User intent: "print the first page with my account summary"
Result: user prints page 1, signs it, files it — never reads the disclosure on page 3
The acceptance was separated from its corresponding obligation disclosure.
page-break-after variant:
.acceptance-block { page-break-after: avoid; }
Prevents the acceptance block from ever appearing immediately after the disclosures.
Forces them to be on different pages even if the browser would naturally place them together. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| display:block on screen-hidden elements in @media print | CSS injection inside @media print block | Reveals content hidden from screen view (internal notes, admin fields, draft text, user data) in the printed document or PDF export — data exposure that bypasses screen-layer access controls | HIGH |
| Force all colors to black — remove color security semantics | CSS injection with color:black !important in @media print | Erases color-coded security status from printed documents — approved/rejected, high/low severity, danger/safe status all appear identical in black monochrome, creating ambiguous or deceptive security records | HIGH |
| display:none on security notices in print layout | CSS injection targeting .warning, [role="alert"], .confidentiality selectors in @media print | Removes security disclosures, draft watermarks, confidentiality notices, and data sensitivity labels from printed output — printed document appears clean/unclassified when screen version carries restrictions | MEDIUM |
| page-break manipulation separating acceptance from disclosure | CSS injection with page-break-before/after and artificial spacers in @media print | Pushes security disclosure or terms sections to later pages — user printing only page 1 sees the acceptance/summary but not the obligations; legal and compliance implications from signed documents with displaced disclosures | MEDIUM |
Defences
- CSP
style-srcwith nonce. Prevents MCP injection of any<style>block, including those containing@media printrules. This is the primary and most effective defence. - Server-side PDF generation. If the application needs to produce PDFs, generate them server-side (via Puppeteer, wkhtmltopdf, or a PDF API) rather than relying on the browser's print dialog. Server-side rendering operates outside the MCP's CSS injection scope.
- Never rely on screen-only hiding for sensitive DOM content. If content should not appear in print, it should not be in the DOM at all for users who should not see it. DOM-hidden content can be exposed by any CSS injection, not just
@media print— the fix is to not render it server-side. - Audit
@media printrules in MCP CSS output. SkillAudit specifically scans for@media printblocks in MCP server stylesheets and flags anydisplay:blockon broad selectors,display:nonetargeting notice/warning selectors,color:black !importantoverrides, andpage-break-before:alwayscombined with padding or height spacers. - SkillAudit flags:
@media printblocks containingdisplay:block !importanton[hidden],.internal, or[data-*]selectors;color:black !importantinside@media print;display:nonetargeting[role="alert"],.warning, or.noticein print context;page-break-before:alwayson disclosure sections combined with height/padding spacers.
SkillAudit findings for this attack surface
Related: CSS media features security covers other @media contexts (prefers-color-scheme, forced-colors) used for fingerprinting. CSS content property security covers ::before/::after content injection. CSS injection overview covers the general attack model.