From 500398b098f32ab54251fcfb180fe66f3c7ff85a Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Wed, 23 Jul 2025 22:30:28 -0700 Subject: [PATCH 01/14] Implement CSS categorization and security checks in the CSS plugin --- package-lock.json | 12 +- packages/markdown/package.json | 5 +- packages/markdown/src/plugins/css.ts | 201 +++++++++++++++++++++--- packages/markdown/test/index.html | 1 + packages/markdown/vite.bundle.config.js | 3 +- packages/sandbox/vite.bundle.config.js | 3 +- 6 files changed, 200 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9d93a30..a9c7a561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4217,6 +4217,13 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/css-tree": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.10.tgz", + "integrity": "sha512-WcaBazJ84RxABvRttQjjFWgTcHvZR9jGr0Y3hccPkHjFyk/a3N8EuxjKr+QfrwjoM5b1yI1Uj1i7EzOAAwBwag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -13163,7 +13170,10 @@ "packages/markdown": { "name": "@microsoft/interactive-document-markdown", "version": "1.0.0", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "@types/css-tree": "^2.3.10" + } }, "packages/renderer": { "name": "@microsoft/interactive-document-renderer", diff --git a/packages/markdown/package.json b/packages/markdown/package.json index 7d1b8026..390f6632 100644 --- a/packages/markdown/package.json +++ b/packages/markdown/package.json @@ -34,5 +34,8 @@ "bugs": { "url": "https://github.com/microsoft/interactive-documents/issues" }, - "homepage": "https://github.com/microsoft/interactive-documents#readme" + "homepage": "https://github.com/microsoft/interactive-documents#readme", + "devDependencies": { + "@types/css-tree": "^2.3.10" + } } diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index f8e8e8b4..27e8e616 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -6,9 +6,150 @@ import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; +// CSS Tree is expected to be available as a global variable +declare const csstree: any; + +interface Rule { + css: string; + flag?: "xss" | "scriptExec" | "externalResource" | "malformed"; + reason?: string; +} + +interface CategorizedCss { + safeAtRules?: { [atRuleSignature: string]: Rule[] }; // Optional - might be empty if all rules are unsafe + flaggedAtRules?: { [atRuleSignature: string]: Rule[] }; // External resources needing approval + unsafeAtRules?: { [atRuleSignature: string]: Rule[] }; // Definitively blocked content +} + +// Helper function to reconstitute an at-rule block from signature and rules +function reconstituteAtRule(signature: string, rules: Rule[], onlyUnflagged: boolean = true): string { + const validRules = onlyUnflagged ? rules.filter(rule => !rule.flag) : rules; + if (validRules.length === 0) return ''; + + if (signature === '') { + // Standalone rules (no at-rule wrapper) + return validRules.map(rule => rule.css).join('\n'); + } else { + // At-rule with contained rules + const containedRules = validRules.map(rule => rule.css).join('\n'); + return `${signature} {\n${containedRules}\n}`; + } +} + +// Helper function to reconstitute complete CSS from categorized rules +function reconstituteCss(atRules: { [atRuleSignature: string]: Rule[] }, onlyUnflagged: boolean = true): string { + const cssBlocks: string[] = []; + + for (const [signature, rules] of Object.entries(atRules)) { + const reconstructed = reconstituteAtRule(signature, rules, onlyUnflagged); + if (reconstructed) { + cssBlocks.push(reconstructed); + } + } + + return cssBlocks.join('\n\n'); +} + +function categorizeCss(cssContent: string): CategorizedCss { + const result: CategorizedCss = {}; + + // Helper function to check for security issues + function checkSecurityIssues(ruleContent: string): Pick | null { + if (ruleContent.toLowerCase().includes('expression(')) { + return { flag: 'scriptExec', reason: 'CSS expression() function detected' }; + } else if (ruleContent.toLowerCase().includes('javascript:')) { + return { flag: 'scriptExec', reason: 'JavaScript URL detected' }; + } else if (ruleContent.includes('http://') || ruleContent.includes('https://')) { + return { flag: 'externalResource', reason: 'External URL detected' }; + } + return null; + } + + // Helper function to add a rule to the appropriate collections + function addRule(signature: string, rule: Rule) { + // Add to safeAtRules + if (!result.safeAtRules) { + result.safeAtRules = {}; + } + if (!result.safeAtRules[signature]) { + result.safeAtRules[signature] = []; + } + result.safeAtRules[signature].push(rule); + + // Add to appropriate flagged/unsafe collection based on flag type + if (rule.flag) { + if (rule.flag === 'externalResource') { + // External resources need whitelist approval + if (!result.flaggedAtRules) { + result.flaggedAtRules = {}; + } + if (!result.flaggedAtRules[signature]) { + result.flaggedAtRules[signature] = []; + } + result.flaggedAtRules[signature].push(rule); + } else { + // scriptExec, xss, malformed are unsafe + if (!result.unsafeAtRules) { + result.unsafeAtRules = {}; + } + if (!result.unsafeAtRules[signature]) { + result.unsafeAtRules[signature] = []; + } + result.unsafeAtRules[signature].push(rule); + } + } + } + + try { + // Parse CSS with CSS Tree + const ast = csstree.parse(cssContent); // Walk through the AST and organize rules + csstree.walk(ast, function(node) { + if (node.type === 'Atrule') { + const atRuleSignature = `@${node.name}${node.prelude ? ` ${csstree.generate(node.prelude)}` : ''}`; + const ruleContent = csstree.generate(node); + const rule: Rule = { css: ruleContent }; + + const securityCheck = checkSecurityIssues(ruleContent); + if (securityCheck) { + rule.flag = securityCheck.flag; + rule.reason = securityCheck.reason; + } + + addRule(atRuleSignature, rule); + + } else if (node.type === 'Rule') { + const ruleContent = csstree.generate(node); + const rule: Rule = { css: ruleContent }; + + const securityCheck = checkSecurityIssues(ruleContent); + if (securityCheck) { + rule.flag = securityCheck.flag; + rule.reason = securityCheck.reason; + } + + addRule('', rule); // Empty string for standalone rules + } + }); + + // Return the categorized result - hydration will decide what to apply + + } catch (parseError) { + result.unsafeAtRules = { + '': [{ css: cssContent, flag: 'malformed', reason: `CSS parsing failed: ${parseError.message}` }] + }; + } + + return result; +} + export const cssPlugin: Plugin = { name: 'css', initializePlugin: (md) => { + // Check for required css-tree dependency + if (typeof csstree === 'undefined') { + throw new Error('css-tree library is required for CSS plugin. Please include the css-tree script.'); + } + definePlugin(md, 'css'); // Custom rule for CSS blocks md.block.ruler.before('fence', 'css_block', function (state, startLine, endLine) { @@ -45,7 +186,12 @@ export const cssPlugin: Plugin = { if (info === 'css') { const cssId = `css-${idx}`; - return sanitizedHTML('div', { id: cssId, class: 'css-component' }, token.content.trim()); + const cssContent = token.content.trim(); + + // Parse and categorize CSS content + const categorizedCss = categorizeCss(cssContent); + + return sanitizedHTML('div', { id: cssId, class: 'css-component' }, JSON.stringify(categorizedCss)); } // Fallback to original fence renderer @@ -64,26 +210,39 @@ export const cssPlugin: Plugin = { if (!container.textContent) continue; try { - const cssContent = container.textContent.trim(); - - // Note: CSS sanitization is handled by DOMPurify at the renderer level - - // Create a style element with a unique ID - const styleElement = document.createElement('style'); - styleElement.type = 'text/css'; - styleElement.id = `idocs-css-${container.id}`; - styleElement.textContent = cssContent; - - // Add the style element to the renderer element - renderer.element.appendChild(styleElement); - - // Clear the container and add a comment indicating CSS was applied - container.innerHTML = ``; - - cssInstances.push({ - id: container.id, - element: styleElement - }); + const categorizedCss: CategorizedCss = JSON.parse(container.textContent); + + // Handle CSS with issues - show appropriate message + if (categorizedCss.unsafeAtRules) { + container.innerHTML = `
CSS blocked due to security issues
`; + continue; + } + + if (categorizedCss.flaggedAtRules) { + container.innerHTML = `
CSS contains external URLs requiring approval
`; + continue; + } + + // Generate and apply safe CSS from unflagged rules + let safeCss = ''; + if (categorizedCss.safeAtRules) { + safeCss = reconstituteCss(categorizedCss.safeAtRules, true); + } + + if (safeCss.trim().length > 0) { + const styleElement = document.createElement('style'); + styleElement.type = 'text/css'; + styleElement.id = `idocs-css-${container.id}`; + styleElement.textContent = safeCss; + + renderer.element.appendChild(styleElement); + container.innerHTML = ``; + + cssInstances.push({ + id: container.id, + element: styleElement + }); + } } catch (e) { container.innerHTML = `
${e.toString()}
`; errorHandler(e, 'CSS', index, 'parse', container); diff --git a/packages/markdown/test/index.html b/packages/markdown/test/index.html index 3206344b..bdf06ab7 100644 --- a/packages/markdown/test/index.html +++ b/packages/markdown/test/index.html @@ -8,6 +8,7 @@ + diff --git a/packages/markdown/vite.bundle.config.js b/packages/markdown/vite.bundle.config.js index 17fa270a..dbff24cc 100644 --- a/packages/markdown/vite.bundle.config.js +++ b/packages/markdown/vite.bundle.config.js @@ -10,6 +10,7 @@ const commonOutputConfig = { 'markdown-it': 'markdownit', 'vega': 'vega', 'vega-lite': 'vegaLite', + 'css-tree': 'csstree', }, entryFileNames: 'idocs.markdown.umd.js', }; @@ -22,7 +23,7 @@ export default defineConfig({ minify: false, rollupOptions: { // External dependencies that the library expects consumers to provide - external: ['markdown-it', 'vega', 'vega-lite', 'tabulator-tables'], + external: ['markdown-it', 'vega', 'vega-lite', 'tabulator-tables', 'css-tree'], output: [ { ...commonOutputConfig, diff --git a/packages/sandbox/vite.bundle.config.js b/packages/sandbox/vite.bundle.config.js index c11e8843..c8b77219 100644 --- a/packages/sandbox/vite.bundle.config.js +++ b/packages/sandbox/vite.bundle.config.js @@ -10,6 +10,7 @@ const commonOutputConfig = { 'markdown-it': 'markdownit', 'vega': 'vega', 'vega-lite': 'vegaLite', + 'css-tree': 'csstree', }, entryFileNames: 'idocs.sandbox.umd.js', }; @@ -23,7 +24,7 @@ export default defineConfig({ emptyOutDir: false, rollupOptions: { // External dependencies that the library expects consumers to provide - external: ['vega', 'vega-lite'], + external: ['vega', 'vega-lite', 'css-tree'], output: [ { ...commonOutputConfig, From 6d4688e9072fb2d028f0a4e0f6c0378b193bf032 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Wed, 23 Jul 2025 22:45:41 -0700 Subject: [PATCH 02/14] Refactor error handling in CSS plugin to use console messages instead of innerHTML for security issues --- packages/markdown/src/plugins/css.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 27e8e616..f3219b58 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -214,12 +214,12 @@ export const cssPlugin: Plugin = { // Handle CSS with issues - show appropriate message if (categorizedCss.unsafeAtRules) { - container.innerHTML = `
CSS blocked due to security issues
`; + console.error('CSS blocked due to security issues'); continue; } if (categorizedCss.flaggedAtRules) { - container.innerHTML = `
CSS contains external URLs requiring approval
`; + console.warn('CSS contains external URLs requiring approval'); continue; } From 31a44d6f218499aa7e73b86dac8ebff7c0b52cf7 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Wed, 23 Jul 2025 23:14:25 -0700 Subject: [PATCH 03/14] remove markdownit, add csstree --- docs/edit/index.html | 1 - docs/view/index.html | 1 - package-lock.json | 22 +++++++++++++++++++ package.json | 1 + packages/editor/index.html | 3 ++- packages/host/index.html | 1 - packages/sandbox/index.html | 1 - packages/sandbox/src/sandbox.ts | 1 + packages/vscode-resources/html/html-json.html | 2 -- .../vscode-resources/html/html-markdown.html | 2 -- packages/vscode/scripts/resources.mjs | 1 + packages/vscode/src/web/command-edit.ts | 2 +- packages/vscode/src/web/resources.ts | 1 + 13 files changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/edit/index.html b/docs/edit/index.html index 2383e309..b02a4bd8 100644 --- a/docs/edit/index.html +++ b/docs/edit/index.html @@ -8,7 +8,6 @@ - diff --git a/docs/view/index.html b/docs/view/index.html index 70a36b19..68394971 100644 --- a/docs/view/index.html +++ b/docs/view/index.html @@ -5,7 +5,6 @@ Interactive Document Host - - - {{SANDBOX_UMD_JS}} diff --git a/packages/vscode-resources/html/html-markdown.html b/packages/vscode-resources/html/html-markdown.html index 5b54efdb..5219de61 100644 --- a/packages/vscode-resources/html/html-markdown.html +++ b/packages/vscode-resources/html/html-markdown.html @@ -37,8 +37,6 @@ } - - {{SANDBOX_UMD_JS}} diff --git a/packages/vscode/scripts/resources.mjs b/packages/vscode/scripts/resources.mjs index 218ae505..170cfa2a 100644 --- a/packages/vscode/scripts/resources.mjs +++ b/packages/vscode/scripts/resources.mjs @@ -7,6 +7,7 @@ const resources = [ '../../node_modules/vega/build/vega.min.js', '../../node_modules/vega-lite/build/vega-lite.min.js', '../../node_modules/markdown-it/dist/markdown-it.min.js', + '../../node_modules/css-tree/dist/csstree.js', '../../node_modules/tabulator-tables/dist/js/tabulator.min.js', '../../node_modules/tabulator-tables/dist/css/tabulator.min.css', '../../packages/host/dist/umd/idocs.host.umd.js', diff --git a/packages/vscode/src/web/command-edit.ts b/packages/vscode/src/web/command-edit.ts index 0130ab8e..327ce094 100644 --- a/packages/vscode/src/web/command-edit.ts +++ b/packages/vscode/src/web/command-edit.ts @@ -64,6 +64,7 @@ export class EditManager { offlineDeps: style(getResourceContent('tabulator.min.css')) + script(getResourceContent('markdown-it.min.js')) + + script(getResourceContent('csstree.js')) + script(getResourceContent('vega.min.js')) + script(getResourceContent('vega-lite.min.js')) + script(getResourceContent('tabulator.min.js')) @@ -200,7 +201,6 @@ function getWebviewContent(webView: vscode.Webview, context: vscode.ExtensionCon const resourceLinks = [ script(resourceUrl('react.production.min.js')), script(resourceUrl('react-dom.production.min.js')), - script(resourceUrl('markdown-it.min.js')), script(resourceUrl('idocs.editor.umd.js')), script(resourceUrl('edit.js')), ].join('\n '); diff --git a/packages/vscode/src/web/resources.ts b/packages/vscode/src/web/resources.ts index 5da3269f..50fcd0e5 100644 --- a/packages/vscode/src/web/resources.ts +++ b/packages/vscode/src/web/resources.ts @@ -14,6 +14,7 @@ export const initializeResources = async (context: vscode.ExtensionContext): Pro //offline copies for editor sandbox (which can't access via vscode resources url) 'tabulator.min.css', 'markdown-it.min.js', + 'csstree.js', 'vega.min.js', 'vega-lite.min.js', 'tabulator.min.js', From c7491e9830f374b0edad984c4ee887c1298fda03 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Wed, 23 Jul 2025 23:14:49 -0700 Subject: [PATCH 04/14] Refactor CSS categorization to improve readability and add security messages for blocked styles --- packages/markdown/src/plugins/css.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index f3219b58..3abd2322 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -103,7 +103,7 @@ function categorizeCss(cssContent: string): CategorizedCss { try { // Parse CSS with CSS Tree const ast = csstree.parse(cssContent); // Walk through the AST and organize rules - csstree.walk(ast, function(node) { + csstree.walk(ast, function (node) { if (node.type === 'Atrule') { const atRuleSignature = `@${node.name}${node.prelude ? ` ${csstree.generate(node.prelude)}` : ''}`; const ruleContent = csstree.generate(node); @@ -205,7 +205,7 @@ export const cssPlugin: Plugin = { hydrateComponent: async (renderer, errorHandler) => { const cssInstances: { id: string; element: HTMLStyleElement }[] = []; const containers = renderer.element.querySelectorAll('.css-component'); - + for (const [index, container] of Array.from(containers).entries()) { if (!container.textContent) continue; @@ -215,11 +215,13 @@ export const cssPlugin: Plugin = { // Handle CSS with issues - show appropriate message if (categorizedCss.unsafeAtRules) { console.error('CSS blocked due to security issues'); + container.innerHTML = ``; continue; } if (categorizedCss.flaggedAtRules) { console.warn('CSS contains external URLs requiring approval'); + container.innerHTML = ``; continue; } From e8cce56175f720b81e2136e70bc3be636af18af1 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Wed, 23 Jul 2025 23:31:07 -0700 Subject: [PATCH 05/14] omit markdown namespace --- packages/sandbox/src/sandbox.ts | 2 +- packages/sandbox/umd.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index fdcbf7cd..fcfa0334 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -2,7 +2,7 @@ import { Previewer, PreviewerOptions } from './preview.js'; import { rendererHtml } from './resources/rendererHtml.js'; import { rendererUmdJs } from './resources/rendererUmdJs.js'; import { sandboxedJs } from './resources/sandboxedJs.js'; -import { RenderRequestMessage } from '@microsoft/interactive-document-markdown'; +import type { RenderRequestMessage } from '@microsoft/interactive-document-markdown'; export class Sandbox extends Previewer { private iframe: HTMLIFrameElement; diff --git a/packages/sandbox/umd.ts b/packages/sandbox/umd.ts index 4f64c63b..254a5c46 100644 --- a/packages/sandbox/umd.ts +++ b/packages/sandbox/umd.ts @@ -1,3 +1,2 @@ export * as common from 'common'; -export * as markdown from '@microsoft/interactive-document-markdown'; export * as sandbox from './src/index.js'; From e8a44f319d97a9e24ec226d17c4c472c510b10ef Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Wed, 23 Jul 2025 23:32:40 -0700 Subject: [PATCH 06/14] new build --- docs/dist/idocs.editor.umd.js | 139 ++- docs/dist/idocs.host.umd.js | 139 ++- docs/dist/idocs.sandbox.umd.js | 1997 +++----------------------------- 3 files changed, 393 insertions(+), 1882 deletions(-) diff --git a/docs/dist/idocs.editor.umd.js b/docs/dist/idocs.editor.umd.js index bb9a2a24..fd5b9cea 100644 --- a/docs/dist/idocs.editor.umd.js +++ b/docs/dist/idocs.editor.umd.js @@ -1149,9 +1149,105 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + function reconstituteAtRule(signature, rules, onlyUnflagged = true) { + const validRules = onlyUnflagged ? rules.filter((rule) => !rule.flag) : rules; + if (validRules.length === 0) return ""; + if (signature === "") { + return validRules.map((rule) => rule.css).join("\\n"); + } else { + const containedRules = validRules.map((rule) => rule.css).join("\\n"); + return \`\${signature} { +\${containedRules} +}\`; + } + } + function reconstituteCss(atRules, onlyUnflagged = true) { + const cssBlocks = []; + for (const [signature, rules] of Object.entries(atRules)) { + const reconstructed = reconstituteAtRule(signature, rules, onlyUnflagged); + if (reconstructed) { + cssBlocks.push(reconstructed); + } + } + return cssBlocks.join("\\n\\n"); + } + function categorizeCss(cssContent) { + const result = {}; + function checkSecurityIssues(ruleContent) { + if (ruleContent.toLowerCase().includes("expression(")) { + return { flag: "scriptExec", reason: "CSS expression() function detected" }; + } else if (ruleContent.toLowerCase().includes("javascript:")) { + return { flag: "scriptExec", reason: "JavaScript URL detected" }; + } else if (ruleContent.includes("http://") || ruleContent.includes("https://")) { + return { flag: "externalResource", reason: "External URL detected" }; + } + return null; + } + function addRule(signature, rule) { + if (!result.safeAtRules) { + result.safeAtRules = {}; + } + if (!result.safeAtRules[signature]) { + result.safeAtRules[signature] = []; + } + result.safeAtRules[signature].push(rule); + if (rule.flag) { + if (rule.flag === "externalResource") { + if (!result.flaggedAtRules) { + result.flaggedAtRules = {}; + } + if (!result.flaggedAtRules[signature]) { + result.flaggedAtRules[signature] = []; + } + result.flaggedAtRules[signature].push(rule); + } else { + if (!result.unsafeAtRules) { + result.unsafeAtRules = {}; + } + if (!result.unsafeAtRules[signature]) { + result.unsafeAtRules[signature] = []; + } + result.unsafeAtRules[signature].push(rule); + } + } + } + try { + const ast = csstree.parse(cssContent); + csstree.walk(ast, function(node) { + if (node.type === "Atrule") { + const atRuleSignature = \`@\${node.name}\${node.prelude ? \` \${csstree.generate(node.prelude)}\` : ""}\`; + const ruleContent = csstree.generate(node); + const rule = { css: ruleContent }; + const securityCheck = checkSecurityIssues(ruleContent); + if (securityCheck) { + rule.flag = securityCheck.flag; + rule.reason = securityCheck.reason; + } + addRule(atRuleSignature, rule); + } else if (node.type === "Rule") { + const ruleContent = csstree.generate(node); + const rule = { css: ruleContent }; + const securityCheck = checkSecurityIssues(ruleContent); + if (securityCheck) { + rule.flag = securityCheck.flag; + rule.reason = securityCheck.reason; + } + addRule("", rule); + } + }); + } catch (parseError) { + result.unsafeAtRules = { + "": [{ css: cssContent, flag: "malformed", reason: \`CSS parsing failed: \${parseError.message}\` }] + }; + } + return result; + } const cssPlugin = { name: "css", initializePlugin: (md) => { + if (typeof csstree === "undefined") { + throw new Error("css-tree library is required for CSS plugin. Please include the css-tree script."); + } definePlugin(md, "css"); md.block.ruler.before("fence", "css_block", function(state, startLine, endLine) { const start = state.bMarks[startLine] + state.tShift[startLine]; @@ -1179,7 +1275,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const info = token.info.trim(); if (info === "css") { const cssId = \`css-\${idx}\`; - return sanitizedHTML("div", { id: cssId, class: "css-component" }, token.content.trim()); + const cssContent = token.content.trim(); + const categorizedCss = categorizeCss(cssContent); + return sanitizedHTML("div", { id: cssId, class: "css-component" }, JSON.stringify(categorizedCss)); } if (originalFence) { return originalFence(tokens, idx, options, env, slf); @@ -1194,17 +1292,33 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy for (const [index2, container] of Array.from(containers).entries()) { if (!container.textContent) continue; try { - const cssContent = container.textContent.trim(); - const styleElement = document.createElement("style"); - styleElement.type = "text/css"; - styleElement.id = \`idocs-css-\${container.id}\`; - styleElement.textContent = cssContent; - renderer.element.appendChild(styleElement); - container.innerHTML = \`\`; - cssInstances.push({ - id: container.id, - element: styleElement - }); + const categorizedCss = JSON.parse(container.textContent); + if (categorizedCss.unsafeAtRules) { + console.error("CSS blocked due to security issues"); + container.innerHTML = \`\`; + continue; + } + if (categorizedCss.flaggedAtRules) { + console.warn("CSS contains external URLs requiring approval"); + container.innerHTML = \`\`; + continue; + } + let safeCss = ""; + if (categorizedCss.safeAtRules) { + safeCss = reconstituteCss(categorizedCss.safeAtRules, true); + } + if (safeCss.trim().length > 0) { + const styleElement = document.createElement("style"); + styleElement.type = "text/css"; + styleElement.id = \`idocs-css-\${container.id}\`; + styleElement.textContent = safeCss; + renderer.element.appendChild(styleElement); + container.innerHTML = \`\`; + cssInstances.push({ + id: container.id, + element: styleElement + }); + } } catch (e) { container.innerHTML = \`
\${e.toString()}
\`; errorHandler(e, "CSS", index2, "parse", container); @@ -2347,6 +2461,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return `