mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-12 13:11:07 +02:00
Drop 'unsafe-inline' from the CSP style-src directives
The print service injected the per-PDF `@page { size }` rule as an inline
<style> element, which required 'unsafe-inline' on style-src-elem.
Inject it through a constructable CSSStyleSheet attached to
document.adoptedStyleSheets instead. Constructable stylesheets aren't
subject to style-src's inline restrictions in browsers.
This commit is contained in:
parent
cb53dbecb9
commit
5ca6026d80
@ -26,6 +26,8 @@ import { makePathFromDrawOPS } from "./display_utils.js";
|
||||
class FontLoader {
|
||||
#systemFonts = new Set();
|
||||
|
||||
#styleSheet = null;
|
||||
|
||||
constructor({
|
||||
ownerDocument = globalThis.document,
|
||||
styleElement = null, // For testing only.
|
||||
@ -55,14 +57,38 @@ class FontLoader {
|
||||
}
|
||||
|
||||
insertRule(rule) {
|
||||
const styleSheet = this.#getStyleSheet();
|
||||
styleSheet.insertRule(rule, styleSheet.cssRules.length);
|
||||
}
|
||||
|
||||
#getStyleSheet() {
|
||||
if (this.#styleSheet) {
|
||||
return this.#styleSheet;
|
||||
}
|
||||
|
||||
// Constructable stylesheets aren't blocked by CSP inline-style checks.
|
||||
// Use the constructor from the document's own window, since
|
||||
// `this._document` may belong to a different window (e.g. a print iframe)
|
||||
// and a constructable stylesheet can only be adopted by the document it was
|
||||
// created for.
|
||||
const StyleSheet =
|
||||
this._document.defaultView?.CSSStyleSheet || globalThis.CSSStyleSheet;
|
||||
if (!this.styleElement && StyleSheet) {
|
||||
const { adoptedStyleSheets } = this._document;
|
||||
if (adoptedStyleSheets) {
|
||||
const styleSheet = new StyleSheet();
|
||||
adoptedStyleSheets.push(styleSheet);
|
||||
return (this.#styleSheet = styleSheet);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.styleElement) {
|
||||
this.styleElement = this._document.createElement("style");
|
||||
this._document.documentElement
|
||||
.getElementsByTagName("head")[0]
|
||||
.append(this.styleElement);
|
||||
}
|
||||
const styleSheet = this.styleElement.sheet;
|
||||
styleSheet.insertRule(rule, styleSheet.cssRules.length);
|
||||
return (this.#styleSheet = this.styleElement.sheet);
|
||||
}
|
||||
|
||||
clear() {
|
||||
@ -72,6 +98,16 @@ class FontLoader {
|
||||
this.nativeFontFaces.clear();
|
||||
this.#systemFonts.clear();
|
||||
|
||||
if (this.#styleSheet) {
|
||||
const { adoptedStyleSheets } = this._document;
|
||||
if (adoptedStyleSheets?.includes(this.#styleSheet)) {
|
||||
this._document.adoptedStyleSheets = adoptedStyleSheets.filter(
|
||||
styleSheet => styleSheet !== this.#styleSheet
|
||||
);
|
||||
}
|
||||
this.#styleSheet = null;
|
||||
}
|
||||
|
||||
if (this.styleElement) {
|
||||
// Note: ChildNode.remove doesn't throw if the parentNode is undefined.
|
||||
this.styleElement.remove();
|
||||
|
||||
@ -1973,17 +1973,14 @@ describe("PDF viewer", () => {
|
||||
null,
|
||||
{
|
||||
earlySetup: () => {
|
||||
// Capture state while window.print() runs — the print service's
|
||||
// destroy() removes the @page stylesheet right after, on the
|
||||
// afterprint event.
|
||||
// Capture state during window.print(): destroy() removes the
|
||||
// @page stylesheet from adoptedStyleSheets right afterwards.
|
||||
window._pageRuleApplied = null;
|
||||
window.print = () => {
|
||||
window._pageRuleApplied = [
|
||||
...document.querySelectorAll("style"),
|
||||
].some(
|
||||
window._pageRuleApplied = document.adoptedStyleSheets.some(
|
||||
s =>
|
||||
s.sheet?.cssRules.length > 0 &&
|
||||
[...s.sheet.cssRules].some(r => r.cssText.includes("@page"))
|
||||
s.cssRules.length > 0 &&
|
||||
[...s.cssRules].some(r => r.cssText.includes("@page"))
|
||||
);
|
||||
};
|
||||
},
|
||||
@ -2007,12 +2004,8 @@ describe("PDF viewer", () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
// The print service injects an inline
|
||||
// <style>@page { size: WxH pt }</style> to match the PDF's page
|
||||
// dimensions. If the CSP `style-src-elem` directive blocks inline
|
||||
// <style> elements, the element is created but its content is never
|
||||
// parsed — `sheet.cssRules` stays empty and the @page rule has no
|
||||
// effect. See web/viewer.html.
|
||||
// The @page rule is injected via a constructable stylesheet, which is
|
||||
// exempt from CSP, so the strict policy in web/viewer.html applies it.
|
||||
it("must apply the injected @page rule (no CSP block)", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
*/
|
||||
|
||||
import { buildGetDocumentParams } from "./test_utils.js";
|
||||
import { FontLoader } from "../../src/display/font_loader.js";
|
||||
import { getDocument } from "../../src/display/api.js";
|
||||
|
||||
function getTopLeftPixel(canvasContext) {
|
||||
@ -206,4 +207,35 @@ describe("custom ownerDocument", function () {
|
||||
canvasFactory.destroy(canvasAndCtx);
|
||||
expect(style.remove.called).toBe(true);
|
||||
});
|
||||
|
||||
it("should use a constructable stylesheet for CSS font rules", function () {
|
||||
const rule =
|
||||
'@font-face {font-family:"foo";src:url(data:font/opentype;base64,AA==)}';
|
||||
|
||||
class MockCSSStyleSheet {
|
||||
cssRules = [];
|
||||
|
||||
insertRule(cssRule, index) {
|
||||
this.cssRules.splice(index, 0, cssRule);
|
||||
}
|
||||
}
|
||||
|
||||
const ownerDocument = {
|
||||
adoptedStyleSheets: [],
|
||||
defaultView: {
|
||||
CSSStyleSheet: MockCSSStyleSheet,
|
||||
},
|
||||
fonts: null,
|
||||
};
|
||||
const fontLoader = new FontLoader({ ownerDocument });
|
||||
|
||||
fontLoader.insertRule(rule);
|
||||
|
||||
expect(ownerDocument.adoptedStyleSheets.length).toBe(1);
|
||||
expect(ownerDocument.adoptedStyleSheets[0].cssRules).toEqual([rule]);
|
||||
|
||||
fontLoader.clear();
|
||||
|
||||
expect(ownerDocument.adoptedStyleSheets.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -170,9 +170,9 @@ class FirefoxPrintService {
|
||||
// Insert a @page + size rule to make sure that the page size is correctly
|
||||
// set. Note that we assume that all pages have the same size, because
|
||||
// variable-size pages are scaled down to the initial page size in Firefox.
|
||||
this.pageStyleSheet = document.createElement("style");
|
||||
this.pageStyleSheet.textContent = `@page { size: ${width}pt ${height}pt;}`;
|
||||
body.append(this.pageStyleSheet);
|
||||
this.pageStyleSheet = new CSSStyleSheet();
|
||||
this.pageStyleSheet.replaceSync(`@page { size: ${width}pt ${height}pt;}`);
|
||||
document.adoptedStyleSheets.push(this.pageStyleSheet);
|
||||
|
||||
if (pdfDocument.isPureXfa) {
|
||||
getXfaHtmlForPrinting(printContainer, pdfDocument);
|
||||
@ -203,7 +203,9 @@ class FirefoxPrintService {
|
||||
body.removeAttribute("data-pdfjsprinting");
|
||||
|
||||
if (this.pageStyleSheet) {
|
||||
this.pageStyleSheet.remove();
|
||||
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
|
||||
styleSheet => styleSheet !== this.pageStyleSheet
|
||||
);
|
||||
this.pageStyleSheet = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,9 +124,9 @@ class PDFPrintService {
|
||||
// In browsers where @page + size is not supported, the next stylesheet
|
||||
// will be ignored and the user has to select the correct paper size in
|
||||
// the UI if wanted.
|
||||
this.pageStyleSheet = document.createElement("style");
|
||||
this.pageStyleSheet.textContent = `@page { size: ${width}pt ${height}pt;}`;
|
||||
body.append(this.pageStyleSheet);
|
||||
this.pageStyleSheet = new CSSStyleSheet();
|
||||
this.pageStyleSheet.replaceSync(`@page { size: ${width}pt ${height}pt;}`);
|
||||
document.adoptedStyleSheets.push(this.pageStyleSheet);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -141,7 +141,9 @@ class PDFPrintService {
|
||||
body.removeAttribute("data-pdfjsprinting");
|
||||
|
||||
if (this.pageStyleSheet) {
|
||||
this.pageStyleSheet.remove();
|
||||
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
|
||||
styleSheet => styleSheet !== this.pageStyleSheet
|
||||
);
|
||||
this.pageStyleSheet = null;
|
||||
}
|
||||
if (this._blobURLs) {
|
||||
|
||||
@ -26,17 +26,10 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>PDF.js viewer</title>
|
||||
|
||||
<!--
|
||||
The print service injects an inline <style>@page { size: … }</style>
|
||||
at print time (web/pdf_print_service.js, web/firefox_print_service.js)
|
||||
to match the PDF's page dimensions. Since the size varies per PDF the
|
||||
content can't be pre-hashed, so style-src-elem allows 'unsafe-inline'.
|
||||
Inline style="…" attributes stay blocked via style-src (no fallback).
|
||||
-->
|
||||
<!--#if MOZCENTRAL-->
|
||||
<!--<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource:; style-src-elem resource: 'unsafe-inline'; img-src resource: blob: data:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
|
||||
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource:; img-src resource: blob: data:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
|
||||
/>-->
|
||||
<!--#endif-->
|
||||
|
||||
|
||||
@ -29,33 +29,26 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<!--#endif-->
|
||||
<title>PDF.js viewer</title>
|
||||
|
||||
<!--
|
||||
The print service injects an inline <style>@page { size: … }</style>
|
||||
at print time (web/pdf_print_service.js, web/firefox_print_service.js)
|
||||
to match the PDF's page dimensions. Since the size varies per PDF the
|
||||
content can't be pre-hashed, so style-src-elem allows 'unsafe-inline'.
|
||||
Inline style="…" attributes stay blocked via style-src (no fallback).
|
||||
-->
|
||||
<!--#if MOZCENTRAL-->
|
||||
<!--<link rel="icon" type="image/svg+xml" href="chrome://global/skin/icons/pdf.svg" />-->
|
||||
<!--<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource:; style-src-elem resource: 'unsafe-inline'; img-src resource: blob: data:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
|
||||
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource:; img-src resource: blob: data:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
|
||||
/>-->
|
||||
<!--#elif TESTING-->
|
||||
<!--<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'; worker-src 'self' blob:; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * blob: data:; base-uri 'self'; form-action 'none';"
|
||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'; worker-src 'self' blob:; style-src 'self'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * blob: data:; base-uri 'self'; form-action 'none';"
|
||||
/>-->
|
||||
<!--#elif CHROME-->
|
||||
<!--<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * file: chrome-extension: blob: data: filesystem: drive:; base-uri 'self'; form-action 'none';"
|
||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * file: chrome-extension: blob: data: filesystem: drive:; base-uri 'self'; form-action 'none';"
|
||||
/>-->
|
||||
<!--#else-->
|
||||
<!--<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * blob: data:; base-uri 'none'; form-action 'none';"
|
||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * blob: data:; base-uri 'none'; form-action 'none';"
|
||||
/>-->
|
||||
<!--#endif-->
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user