diff --git a/docs/dist/idocs.editor.umd.js b/docs/dist/idocs.editor.umd.js index 02706c37..5fa23b4a 100644 --- a/docs/dist/idocs.editor.umd.js +++ b/docs/dist/idocs.editor.umd.js @@ -1187,6 +1187,27 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy atRules: {}, hasFlags: false }; + const completeBlockAtRules = [ + // Keyframes and variants + "keyframes", + "-webkit-keyframes", + "-moz-keyframes", + "-o-keyframes", + // Font-related at-rules + "font-face", + "font-feature-values", + "font-palette-values", + // Page and counter styling + "page", + "counter-style", + // CSS Houdini and newer features + "property", + "layer", + "container", + "scope", + "starting-style", + "position-try" + ]; function checkSecurityIssues(node) { if (node.type === "Function" && node.name === "expression") { return { flag: "scriptExec", reason: "CSS expression() function detected" }; @@ -1259,6 +1280,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy result.hasFlags = true; return; } + if (completeBlockAtRules.includes(node.name)) { + const ruleContent = csstree.generate(node); + result.atRules[atRuleSignature] = { + signature: atRuleSignature, + css: ruleContent + }; + return; + } if (node.block) { if (!result.atRules[atRuleSignature]) { result.atRules[atRuleSignature] = { diff --git a/docs/dist/idocs.host.umd.js b/docs/dist/idocs.host.umd.js index eb173a4a..9af57e42 100644 --- a/docs/dist/idocs.host.umd.js +++ b/docs/dist/idocs.host.umd.js @@ -1187,6 +1187,27 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy atRules: {}, hasFlags: false }; + const completeBlockAtRules = [ + // Keyframes and variants + "keyframes", + "-webkit-keyframes", + "-moz-keyframes", + "-o-keyframes", + // Font-related at-rules + "font-face", + "font-feature-values", + "font-palette-values", + // Page and counter styling + "page", + "counter-style", + // CSS Houdini and newer features + "property", + "layer", + "container", + "scope", + "starting-style", + "position-try" + ]; function checkSecurityIssues(node) { if (node.type === "Function" && node.name === "expression") { return { flag: "scriptExec", reason: "CSS expression() function detected" }; @@ -1259,6 +1280,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy result.hasFlags = true; return; } + if (completeBlockAtRules.includes(node.name)) { + const ruleContent = csstree.generate(node); + result.atRules[atRuleSignature] = { + signature: atRuleSignature, + css: ruleContent + }; + return; + } if (node.block) { if (!result.atRules[atRuleSignature]) { result.atRules[atRuleSignature] = { @@ -2775,54 +2804,65 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function checkUrlForFile(host) { const urlParams = new URLSearchParams(window.location.search); const loadUrl = urlParams.get(host.options.urlParamName); + if (!loadUrl) { + return false; + } + if (!isValidLoadUrl(loadUrl)) { + host.errorHandler( + new Error("Invalid URL format"), + "The URL provided has an invalid format or contains suspicious characters." + ); + return false; + } const isValidUrl = (url) => isSameOrigin(url) || isHttps(url); - if (loadUrl && !isValidUrl(loadUrl)) { + if (!isValidUrl(loadUrl)) { host.errorHandler( new Error(`Invalid URL provided`), "The URL provided is not valid. Please ensure it is on the same origin or uses HTTPS." ); return false; } - if (loadUrl) { - if (!isValidLoadUrl(loadUrl)) { - host.errorHandler( - new Error("Invalid URL format"), - "The URL provided has an invalid format or contains suspicious characters." - ); - return false; - } - try { - fetch(loadUrl).then((response) => { - if (!response.ok) { - throw new Error(`Failed to load content from URL`); - } - return response.text(); - }).then((content) => { - determineContent(content, host); - }).catch((error) => { - host.errorHandler(error, `Error loading file from the provided URL`); - }); - } catch (error) { + try { + fetch(loadUrl).then((response) => { + if (!response.ok) { + throw new Error(`Failed to load content from URL`); + } + return response.text(); + }).then((content) => { + determineContent(content, host); + }).catch((error) => { host.errorHandler(error, `Error loading file from the provided URL`); - } - return true; - } else { - return false; + }); + } catch (error) { + host.errorHandler(error, `Error loading file from the provided URL`); } + return true; } function isSameOrigin(url) { - const link = document.createElement("a"); - link.href = url; - return link.origin === window.location.origin; + try { + if (!url.includes("://")) { + return true; + } + const parsedUrl = new URL(url); + return parsedUrl.origin === window.location.origin; + } catch { + return false; + } } function isHttps(url) { - const link = document.createElement("a"); - link.href = url; - return link.protocol === "https:"; + try { + if (!url.includes("://")) { + return window.location.protocol === "https:"; + } + const parsedUrl = new URL(url); + return parsedUrl.protocol === "https:"; + } catch { + return false; + } } function isValidLoadUrl(url) { try { - const parsedUrl = new URL(url); + const parsedUrl = new URL(url, window.location.href); if (!["http:", "https:"].includes(parsedUrl.protocol)) { return false; } diff --git a/docs/dist/idocs.sandbox.umd.js b/docs/dist/idocs.sandbox.umd.js index bb9ed8cd..cb94d9bf 100644 --- a/docs/dist/idocs.sandbox.umd.js +++ b/docs/dist/idocs.sandbox.umd.js @@ -787,6 +787,27 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy atRules: {}, hasFlags: false }; + const completeBlockAtRules = [ + // Keyframes and variants + "keyframes", + "-webkit-keyframes", + "-moz-keyframes", + "-o-keyframes", + // Font-related at-rules + "font-face", + "font-feature-values", + "font-palette-values", + // Page and counter styling + "page", + "counter-style", + // CSS Houdini and newer features + "property", + "layer", + "container", + "scope", + "starting-style", + "position-try" + ]; function checkSecurityIssues(node) { if (node.type === "Function" && node.name === "expression") { return { flag: "scriptExec", reason: "CSS expression() function detected" }; @@ -859,6 +880,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy result.hasFlags = true; return; } + if (completeBlockAtRules.includes(node.name)) { + const ruleContent = csstree.generate(node); + result.atRules[atRuleSignature] = { + signature: atRuleSignature, + css: ruleContent + }; + return; + } if (node.block) { if (!result.atRules[atRuleSignature]) { result.atRules[atRuleSignature] = { diff --git a/packages/host/src/url.ts b/packages/host/src/url.ts index 89857305..10015c28 100644 --- a/packages/host/src/url.ts +++ b/packages/host/src/url.ts @@ -5,10 +5,22 @@ export function checkUrlForFile(host: Listener) { const urlParams = new URLSearchParams(window.location.search); const loadUrl = urlParams.get(host.options.urlParamName); - //ensure loadUrl is on the same origin, or must be https - const isValidUrl = (url: string) => isSameOrigin(url) || isHttps(url); + if (!loadUrl) { + return false; // No load parameter found + } + + // First, validate the URL format and security + if (!isValidLoadUrl(loadUrl)) { + host.errorHandler( + new Error('Invalid URL format'), + 'The URL provided has an invalid format or contains suspicious characters.' + ); + return false; + } - if (loadUrl && !isValidUrl(loadUrl)) { + // Then check origin/protocol requirements + const isValidUrl = (url: string) => isSameOrigin(url) || isHttps(url); + if (!isValidUrl(loadUrl)) { host.errorHandler( new Error(`Invalid URL provided`), 'The URL provided is not valid. Please ensure it is on the same origin or uses HTTPS.' @@ -16,73 +28,79 @@ export function checkUrlForFile(host: Listener) { return false; } - if (loadUrl) { - // Additional URL validation - if (!isValidLoadUrl(loadUrl)) { - host.errorHandler( - new Error('Invalid URL format'), - 'The URL provided has an invalid format or contains suspicious characters.' - ); - return false; - } - - try { - - fetch(loadUrl) - .then(response => { - if (!response.ok) { - throw new Error(`Failed to load content from URL`); - } - return response.text(); - }) - .then(content => { - determineContent(content, host); - }) - .catch(error => { - host.errorHandler(error as Error, `Error loading file from the provided URL`); - }); + try { + fetch(loadUrl) + .then(response => { + if (!response.ok) { + throw new Error(`Failed to load content from URL`); + } + return response.text(); + }) + .then(content => { + determineContent(content, host); + }) + .catch(error => { + host.errorHandler(error as Error, `Error loading file from the provided URL`); + }); - } catch (error) { - host.errorHandler(error as Error, `Error loading file from the provided URL`); - } - return true; // We found a load parameter - } else { - return false; // No load parameter found + } catch (error) { + host.errorHandler(error as Error, `Error loading file from the provided URL`); } + return true; // We found a load parameter } function isSameOrigin(url: string) { - const link = document.createElement("a"); - link.href = url; - return link.origin === window.location.origin; + try { + // First check if it's a relative URL (no protocol) + if (!url.includes('://')) { + // Relative URLs are inherently same-origin + return true; + } + + // For absolute URLs, check if origin matches + const parsedUrl = new URL(url); + return parsedUrl.origin === window.location.origin; + } catch { + return false; + } } function isHttps(url: string) { - const link = document.createElement("a"); - link.href = url; - return link.protocol === "https:"; + try { + // Relative URLs inherit the current page's protocol + if (!url.includes('://')) { + return window.location.protocol === "https:"; + } + + // For absolute URLs, check the protocol directly + const parsedUrl = new URL(url); + return parsedUrl.protocol === "https:"; + } catch { + return false; + } } function isValidLoadUrl(url: string): boolean { try { - const parsedUrl = new URL(url); - + // Resolve relative URLs against current location + const parsedUrl = new URL(url, window.location.href); + // Only allow http and https protocols if (!['http:', 'https:'].includes(parsedUrl.protocol)) { return false; } - - // Prevent javascript:, vbscript:, and data: URLs + + // Prevent javascript:, vbscript:, and data: URLs (redundant check but good for clarity) if (parsedUrl.protocol === 'javascript:' || parsedUrl.protocol === 'vbscript:' || parsedUrl.protocol === 'data:') { return false; } - + // Basic hostname validation (prevent obvious malicious patterns) const hostname = parsedUrl.hostname.toLowerCase(); if (hostname.includes('<') || hostname.includes('>') || hostname.includes('"') || hostname.includes("'")) { return false; } - + return true; } catch { return false; diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 837f07f3..660a8f2b 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -38,7 +38,7 @@ interface CategorizedCss { // Helper function to reconstitute an at-rule block from at-rule data function reconstituteAtRule(atRule: AtRule): string { if (atRule.css) { - // Simple at-rule (like @import) - check if it's flagged + // Simple at-rule (like @import) or complete at-rule (like @keyframes) - check if it's flagged return atRule.flag ? `/* ${atRule.css} - BLOCKED: ${atRule.reason} */` : atRule.css; } @@ -88,6 +88,29 @@ function categorizeCss(cssContent: string): CategorizedCss { hasFlags: false }; + // At-rules that should be treated as complete blocks (not parsed internally) + const completeBlockAtRules = [ + // Keyframes and variants + 'keyframes', + '-webkit-keyframes', + '-moz-keyframes', + '-o-keyframes', + // Font-related at-rules + 'font-face', + 'font-feature-values', + 'font-palette-values', + // Page and counter styling + 'page', + 'counter-style', + // CSS Houdini and newer features + 'property', + 'layer', + 'container', + 'scope', + 'starting-style', + 'position-try' + ]; + // Helper function to check for security issues using AST node analysis function checkSecurityIssues(node: any): Pick | null { // Check for script execution in CSS expressions @@ -192,7 +215,18 @@ function categorizeCss(cssContent: string): CategorizedCss { return; } - // For other at-rules that contain rules (like @media, @keyframes) + // For at-rules that should be treated as complete blocks + if (completeBlockAtRules.includes(node.name)) { + // Store the entire rule as CSS and validate it as a complete block + const ruleContent = csstree.generate(node); + result.atRules[atRuleSignature] = { + signature: atRuleSignature, + css: ruleContent + }; + return; + } + + // For other at-rules that contain rules (like @media, @supports) if (node.block) { if (!result.atRules[atRuleSignature]) { result.atRules[atRuleSignature] = {