Skip to content

Commit

Permalink
feat: support HTML style tags scoping
Browse files Browse the repository at this point in the history
Related to #1004
  • Loading branch information
Skaiir committed Jan 29, 2024
1 parent 5ab82b8 commit 8568751
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ function Content(props) {
// allow expressions
if (value.startsWith('=')) { return null; }

// disallow style tags
if (value.includes('<style')) { return 'Style tags may not be defined here, please use inline styling.'; }
};

return FeelTemplatingEntry({
Expand All @@ -71,4 +69,4 @@ function Content(props) {
});
}

const description = <>Supports HTML, inline styling, and templating. <a href="https://docs.camunda.io/docs/components/modeler/forms/form-element-library/forms-element-library-html/" target="_blank">Learn more</a></>;
const description = <>Supports HTML, styling, and templating. Styles are automatically scoped to the HTML component. <a href="https://docs.camunda.io/docs/components/modeler/forms/form-element-library/forms-element-library-html/" target="_blank">Learn more</a></>;
35 changes: 30 additions & 5 deletions packages/form-js-viewer/src/render/components/form-fields/Html.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ export function Html(props) {
const form = useService('form');
const { textLinkTarget } = form._getState().properties;

const { field, disableLinks } = props;
const { field, disableLinks, domId } = props;

const { content = '', strict = false } = field;

const styleScopeId = `${domId}-style-scope`;

// we escape HTML within the template evaluation to prevent clickjacking attacks
const html = useTemplateEvaluation(content, { debug: true, strict, sanitizer: escapeHTML });

const transformLinks = useCallback((html) => {
const transform = useCallback((html) => {

const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;

// (1) apply modifications to links

const links = tempDiv.querySelectorAll('a');

links.forEach(link => {
Expand All @@ -41,12 +45,33 @@ export function Html(props) {

});

// (2) scope styles to the root div

const styleTags = tempDiv.querySelectorAll('style');

styleTags.forEach(styleTag => {
const scopedCss = styleTag.textContent
.split('}')
.map(rule => {
if (!rule.trim()) return '';
const [ selector, styles ] = rule.split('{');
const scopedSelector = selector
.split(',')
.map(sel => `#${styleScopeId} ${sel.trim()}`)
.join(', ');
return `${scopedSelector} { ${styles}`;
})
.join('}');

styleTag.textContent = scopedCss;
});

return tempDiv.innerHTML;

}, [ disableLinks, textLinkTarget ]);
}, [ disableLinks, styleScopeId, textLinkTarget ]);

return <div class={ formFieldClasses(type) }>
<RawHTMLRenderer html={ html } transform={ transformLinks } sanitize={ true } sanitizeStyleTags={ true } />
return <div id={ styleScopeId } class={ formFieldClasses(type) }>
<RawHTMLRenderer html={ html } transform={ transform } sanitize={ true } sanitizeStyleTags={ false } />
</div>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function sanitizeIFrameSource(src) {
* @returns {string}
*/
export function escapeHTML(html) {
return html.replace(/[&<>"']/g, match => {
return html.replace(/[&<>"'{};:]/g, match => {
switch (match) {
case '&':
return '&amp;';
Expand All @@ -112,6 +112,14 @@ export function escapeHTML(html) {
return '&gt;';
case '"':
return '&quot;';
case '{':
return '&#123;';
case '}':
return '&#125;';
case ':':
return '&#58;';
case ';':
return '&#59;';
default:
return '&#039;';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sanitizeDateTimePickerValue, sanitizeImageSource } from '../../../../../../src/render/components/util/sanitizerUtil.js';
import { sanitizeDateTimePickerValue, sanitizeImageSource, escapeHTML } from '../../../../../../src/render/components/util/sanitizerUtil.js';

describe('sanitizerUtil', function() {

Expand Down Expand Up @@ -58,4 +58,47 @@ describe('sanitizerUtil', function() {

});


describe('#escapeHTML', function() {

it('should escape HTML', function() {

// given
const html = '<b>foo</b>';

// when
const escaped = escapeHTML(html);

// then
expect(escaped).to.equal('&lt;b&gt;foo&lt;/b&gt;');
});


it('should escape HTML injection', function() {

// given
const html = '<img src=x onerror=alert(1)//>';

// when
const escaped = escapeHTML(html);

// then
expect(escaped).to.equal('&lt;img src=x onerror=alert(1)//&gt;');
});


it('should escape CSS special characters ({};:)', function() {

// given
const html = '} * { display: none;';

// when
const escaped = escapeHTML(html);

// then
expect(escaped).to.equal('&#125; * &#123; display&#58; none&#59;');
});

});

});

0 comments on commit 8568751

Please sign in to comment.