fix(builder): neutralize javascript:/breakout URLs in export
Every user-controlled URL emitted by a component's static toHtml (href, src, action, and CSS url()) now runs through escapeAttr(safeUrl(...)) before hitting the exported HTML string, closing the XSS gaps flagged in the A2 review (PricingTable buttonHref was fully unescaped, Gallery/ HeroSimple/ImageBlock/VideoBlock/etc. lacked scheme filtering) plus a few more found via a grep sweep of href=/src=/action=/url( inside toHtml template strings: MapEmbed's iframe src and the shared form-relay-wiring fallback form action (a javascript: form action executes on submit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
interface ButtonLinkProps {
|
||||
text?: string;
|
||||
@@ -227,6 +228,6 @@ ButtonLink.craft = {
|
||||
const escapedText = (props.text || '').replace(/</g, '<').replace(/>/g, '>');
|
||||
const targetAttr = props.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
|
||||
return {
|
||||
html: `<a href="${props.href || '#'}"${targetAttr}${styleStr ? ` style="${styleStr}"` : ''}>${escapedText}</a>`,
|
||||
html: `<a href="${escapeAttr(safeUrl(props.href || '#'))}"${targetAttr}${styleStr ? ` style="${styleStr}"` : ''}>${escapedText}</a>`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
interface IconProps {
|
||||
icon?: string;
|
||||
@@ -300,7 +301,7 @@ Icon.craft = {
|
||||
} = props;
|
||||
|
||||
const iconStyle = cssPropsToString({ fontSize: size, color, lineHeight: '1' });
|
||||
let iconHtml = `<i class="fa ${icon}"${iconStyle ? ` style="${iconStyle}"` : ''}></i>`;
|
||||
let iconHtml = `<i class="fa ${escapeAttr(icon)}"${iconStyle ? ` style="${iconStyle}"` : ''}></i>`;
|
||||
|
||||
const hasBg = bgShape !== 'none' && bgColor !== 'transparent';
|
||||
if (hasBg) {
|
||||
@@ -317,7 +318,7 @@ Icon.craft = {
|
||||
}
|
||||
|
||||
if (link) {
|
||||
iconHtml = `<a href="${link}" style="text-decoration:none;color:inherit">${iconHtml}</a>`;
|
||||
iconHtml = `<a href="${escapeAttr(safeUrl(link))}" style="text-decoration:none;color:inherit">${iconHtml}</a>`;
|
||||
}
|
||||
|
||||
const wrapperStyle = cssPropsToString({ display: 'inline-block', ...style });
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { CSSProperties, useCallback, useRef, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { useSiteDesign } from '../../state/SiteDesignContext';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
/* ---------- Types ---------- */
|
||||
|
||||
@@ -389,7 +389,7 @@ Logo.craft = {
|
||||
let innerHtml: string;
|
||||
if (props.type === 'image' && props.imageSrc) {
|
||||
const imgStyle = cssPropsToString({ width: props.imageWidth || '120px', height: 'auto', display: 'block' });
|
||||
innerHtml = `<img src="${escapeAttr(props.imageSrc)}" alt="${escapeAttr(props.text || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} />`;
|
||||
innerHtml = `<img src="${escapeAttr(safeUrl(props.imageSrc))}" alt="${escapeAttr(props.text || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} />`;
|
||||
} else {
|
||||
const spanStyle = cssPropsToString({
|
||||
fontWeight: props.fontWeight || '700',
|
||||
@@ -409,6 +409,6 @@ Logo.craft = {
|
||||
});
|
||||
|
||||
return {
|
||||
html: `<a href="${escapeAttr(href)}"${aStyle ? ` style="${aStyle}"` : ''}>${innerHtml}</a>`,
|
||||
html: `<a href="${escapeAttr(safeUrl(href))}"${aStyle ? ` style="${aStyle}"` : ''}>${innerHtml}</a>`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { CSSProperties, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { usePages } from '../../state/PageContext';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
/* ---------- Types ---------- */
|
||||
|
||||
@@ -489,7 +489,7 @@ Menu.craft = {
|
||||
borderRadius: link.isCta ? '6px' : '0',
|
||||
transition: 'color 0.15s, background-color 0.15s',
|
||||
});
|
||||
return `<a href="${escapeAttr(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||
return `<a href="${escapeAttr(safeUrl(link.href || '#'))}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||
}).join('\n ');
|
||||
|
||||
const hoverCss = `<style>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { usePages } from '../../state/PageContext';
|
||||
import { useSiteDesign } from '../../state/SiteDesignContext';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
/* ---------- Types ---------- */
|
||||
|
||||
@@ -848,7 +848,7 @@ Navbar.craft = {
|
||||
let logoHtml: string;
|
||||
if (props.logoType === 'image' && props.logoImage) {
|
||||
const imgStyle = cssPropsToString({ width: props.logoWidth || '120px', height: 'auto', display: 'block' });
|
||||
logoHtml = `<a href="${escapeAttr(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><img src="${escapeAttr(props.logoImage)}" alt="${escapeAttr(props.logoText || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} /></a>`;
|
||||
logoHtml = `<a href="${escapeAttr(safeUrl(logoUrl))}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><img src="${escapeAttr(safeUrl(props.logoImage))}" alt="${escapeAttr(props.logoText || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} /></a>`;
|
||||
} else {
|
||||
const logoStyle = cssPropsToString({
|
||||
fontWeight: '700',
|
||||
@@ -856,7 +856,7 @@ Navbar.craft = {
|
||||
fontFamily: props.logoFontFamily || 'Inter, sans-serif',
|
||||
color: props.logoColor || textCol,
|
||||
});
|
||||
logoHtml = `<a href="${escapeAttr(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><span${logoStyle ? ` style="${logoStyle}"` : ''}>${escapeHtml(props.logoText || 'MySite')}</span></a>`;
|
||||
logoHtml = `<a href="${escapeAttr(safeUrl(logoUrl))}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><span${logoStyle ? ` style="${logoStyle}"` : ''}>${escapeHtml(props.logoText || 'MySite')}</span></a>`;
|
||||
}
|
||||
|
||||
// Links HTML
|
||||
@@ -873,7 +873,7 @@ Navbar.craft = {
|
||||
borderRadius: link.isCta ? '6px' : '0',
|
||||
transition: 'color 0.15s, background-color 0.15s',
|
||||
});
|
||||
return `<a href="${escapeAttr(link.href)}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||
return `<a href="${escapeAttr(safeUrl(link.href || "#"))}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||
}).join('\n ');
|
||||
|
||||
// Hamburger HTML for mobile
|
||||
@@ -910,7 +910,7 @@ Navbar.craft = {
|
||||
borderRadius: link.isCta ? '6px' : '0',
|
||||
transition: 'color 0.15s, background-color 0.15s',
|
||||
});
|
||||
return `<a href="${escapeAttr(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||
return `<a href="${escapeAttr(safeUrl(link.href || "#"))}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||
}).join('\n ');
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
interface SocialLink {
|
||||
platform: string;
|
||||
@@ -437,7 +438,7 @@ SocialLinks.craft = {
|
||||
if (hasBg) {
|
||||
aStyle += `;${getShapeStr()}`;
|
||||
}
|
||||
return `<a href="${link.url || '#'}" target="_blank" rel="noopener noreferrer" title="${title}" style="${aStyle}"><i class="fa ${iconClass}" style="font-size:${iconSize}"></i></a>`;
|
||||
return `<a href="${escapeAttr(safeUrl(link.url || '#'))}" target="_blank" rel="noopener noreferrer" title="${escapeAttr(title)}" style="${aStyle}"><i class="fa ${escapeAttr(iconClass)}" style="font-size:${iconSize}"></i></a>`;
|
||||
}).join('\n ');
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user