Skip to content
Permalink
Browse files Browse the repository at this point in the history
fix: Security fix for Cross-Site Scripting Vulnerability in the legen…
…d fields

* fix: Security fix for Cross-Site Scripting Vulnerability in the legend fields

* fix: types and style

* fix: all errors and tested
  • Loading branch information
arjunshibu committed Mar 12, 2021
1 parent 8e56e5b commit 1a3f455
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 2 deletions.
5 changes: 3 additions & 2 deletions apps/chart/src/component/tooltip.ts
Expand Up @@ -14,6 +14,7 @@ import { isBoolean, isNumber, isString } from '@src/helpers/utils';
import { SeriesDataType, TooltipTemplateFunc, TooltipFormatter } from '@t/options';
import { TooltipTheme } from '@t/theme';
import { getTranslateString } from '@src/helpers/style';
import sanitizeHtml from '@src/helpers/htmlSanitizer';

type TooltipInfoModels = { [key in TooltipModelName]: TooltipInfo[] };

Expand Down Expand Up @@ -140,8 +141,8 @@ export default class Tooltip extends Component {
this.tooltipContainerEl.innerHTML = this.templateFunc(
model,
{
header: tooltipTemplates.defaultHeader(model, this.theme),
body: getBodyTemplate(model.templateType)(model, this.theme),
header: sanitizeHtml(tooltipTemplates.defaultHeader(model, this.theme), true),
body: sanitizeHtml(getBodyTemplate(model.templateType)(model, this.theme), true),
},
this.theme
);
Expand Down
59 changes: 59 additions & 0 deletions apps/chart/src/helpers/dom.ts
@@ -0,0 +1,59 @@
/**
* @fileoverview DOM Utils
* @author NHN FE Development Lab <dl_javascript@nhn.com>
*/
import { toArray } from './utils';

/**
* Find nodes matching by selector
* @param {HTMLElement} element - target element
* @param {string} selector - selector to find nodes
* @returns {Array.<Node>} found nodes
* @ignore
*/
export function findNodes(element: HTMLElement, selector: string) {
const nodeList = toArray(element.querySelectorAll(selector));

if (nodeList.length) {
return nodeList;
}

return [];
}

/**
* Removes target node from parent node
* @param {Node} node - target node
* @ignore
*/
export function removeNode(node: Node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}

/**
* Finalize html result
* @param {Element} html root element
* @param {boolean} needHtmlText pass true if need html text
* @returns {string|DocumentFragment} result
* @ignore
*/
export function finalizeHtml(html: Element, needHtmlText: boolean) {
let result;

if (needHtmlText) {
result = html.innerHTML;
} else {
const frag = document.createDocumentFragment();
const childNodes = toArray(html.childNodes);
const { length } = childNodes;

for (let i = 0; i < length; i += 1) {
frag.appendChild(childNodes[i]);
}
result = frag;
}

return result;
}
129 changes: 129 additions & 0 deletions apps/chart/src/helpers/htmlSanitizer.ts
@@ -0,0 +1,129 @@
/**
* @fileoverview Implements htmlSanitizer
* @author NHN FE Development Lab <dl_javascript@nhn.com>
*/
import { finalizeHtml, findNodes, removeNode } from './dom';
import { isString, toArray } from './utils';

const HTML_ATTR_LIST_RX = new RegExp(
'^(abbr|align|alt|axis|bgcolor|border|cellpadding|cellspacing|class|clear|' +
'color|cols|compact|coords|dir|face|headers|height|hreflang|hspace|' +
'ismap|lang|language|nohref|nowrap|rel|rev|rows|rules|' +
'scope|scrolling|shape|size|span|start|summary|tabindex|target|title|type|' +
'valign|value|vspace|width|checked|mathvariant|encoding|id|name|' +
'background|cite|href|longdesc|src|usemap|xlink:href|data-+|checked|style)',
'g'
);

const SVG_ATTR_LIST_RX = new RegExp(
'^(accent-height|accumulate|additive|alphabetic|arabic-form|ascent|' +
'baseProfile|bbox|begin|by|calcMode|cap-height|class|color|color-rendering|content|' +
'cx|cy|d|dx|dy|descent|display|dur|end|fill|fill-rule|font-family|font-size|font-stretch|' +
'font-style|font-variant|font-weight|from|fx|fy|g1|g2|glyph-name|gradientUnits|hanging|' +
'height|horiz-adv-x|horiz-origin-x|ideographic|k|keyPoints|keySplines|keyTimes|lang|' +
'marker-end|marker-mid|marker-start|markerHeight|markerUnits|markerWidth|mathematical|' +
'max|min|offset|opacity|orient|origin|overline-position|overline-thickness|panose-1|' +
'path|pathLength|points|preserveAspectRatio|r|refX|refY|repeatCount|repeatDur|' +
'requiredExtensions|requiredFeatures|restart|rotate|rx|ry|slope|stemh|stemv|stop-color|' +
'stop-opacity|strikethrough-position|strikethrough-thickness|stroke|stroke-dasharray|' +
'stroke-dashoffset|stroke-linecap|stroke-linejoin|stroke-miterlimit|stroke-opacity|' +
'stroke-width|systemLanguage|target|text-anchor|to|transform|type|u1|u2|underline-position|' +
'underline-thickness|unicode|unicode-range|units-per-em|values|version|viewBox|visibility|' +
'width|widths|x|x-height|x1|x2|xlink:actuate|xlink:arcrole|xlink:role|xlink:show|xlink:title|' +
'xlink:type|xml:base|xml:lang|xml:space|xmlns|xmlns:xlink|y|y1|y2|zoomAndPan)',
'g'
);

const XSS_ATTR_RX = /href|src|background/gi;
const XSS_VALUE_RX = /((java|vb|live)script|x):/gi;
const ON_EVENT_RX = /^on\S+/;

/**
* htmlSanitizer
* @param {string} html - html
* @param {boolean} [needHtmlText] - pass true if need html text
* @returns {string} - result
* @ignore
*/
function htmlSanitizer(html: string, needHtmlText: boolean) {
const root = document.createElement('div');

if (isString(html)) {
html = html.replace(/<!--[\s\S]*?-->/g, '');
root.innerHTML = html;
} else {
root.appendChild(html);
}

removeUnnecessaryTags(root);
leaveOnlyWhitelistAttribute(root);

return finalizeHtml(root, needHtmlText);
}

/**
* Removes unnecessary tags.
* @param {HTMLElement} html - root element
* @private
*/
function removeUnnecessaryTags(html: HTMLElement) {
const removedTags = findNodes(
html,
'script, iframe, textarea, form, button, select, input, meta, style, link, title, embed, object, details, summary'
);

removedTags.forEach((node) => {
removeNode(node);
});
}

/**
* Checks whether the attribute and value that causing XSS or not.
* @param {string} attrName - name of attribute
* @param {string} attrValue - value of attirbute
* @private
*/
function isXSSAttribute(attrName: string, attrValue: string) {
return attrName.match(XSS_ATTR_RX) && attrValue.match(XSS_VALUE_RX);
}

/**
* Removes attributes of blacklist from node.
* @param {HTMLElement} node - node to remove attributes
* @param {Array<NodeList>} blacklistAttrs - attributes of blacklist
* @private
*/
function removeBlacklistAttributes(node: HTMLElement, blacklistAttrs: Array<NodeList>) {
toArray(blacklistAttrs).forEach(({ name }) => {
if (ON_EVENT_RX.test(name)) {
node[name] = null;
}

if (node.getAttribute(name)) {
node.removeAttribute(name);
}
});
}

/**
* Leaves only white list attributes.
* @param {HTMLElement} html - root element
* @private
*/
function leaveOnlyWhitelistAttribute(html: HTMLElement) {
findNodes(html, '*').forEach((node) => {
const { attributes } = node;
const blacklist = toArray(attributes).filter((attr) => {
const { name, value } = attr;
const htmlAttr = name.match(HTML_ATTR_LIST_RX);
const svgAttr = name.match(SVG_ATTR_LIST_RX);
const xssAttr = htmlAttr && isXSSAttribute(name, value);

return (!htmlAttr && !svgAttr) || xssAttr;
});

removeBlacklistAttributes(node, blacklist);
});
}

export default htmlSanitizer;
27 changes: 27 additions & 0 deletions apps/chart/src/helpers/utils.ts
Expand Up @@ -54,6 +54,19 @@ export function forEach<T extends object, K extends Extract<keyof T, string>, V
}
}

export function forEachArray(arr: NodeList, iteratee: Function, context: any) {
var index = 0;
var len = arr.length;

context = context || null;

for (; index < len; index += 1) {
if (iteratee.call(context, arr[index], index, arr) === false) {
break;
}
}
}

export function range(start: number, stop?: number, step?: number) {
if (isUndefined(stop)) {
stop = start || 0;
Expand All @@ -76,6 +89,20 @@ export function range(start: number, stop?: number, step?: number) {
return arr;
}

export function toArray(arrayLike: any): Array<any> {
var arr;
try {
arr = Array.prototype.slice.call(arrayLike);
} catch (e) {
arr = [];
forEachArray(arrayLike, function(value) {
arr.push(value);
}, null);
}

return arr;
}

export function includes<T>(arr: T[], searchItem: T, searchIndex?: number) {
if (typeof searchIndex === 'number' && arr[searchIndex] !== searchItem) {
return false;
Expand Down

0 comments on commit 1a3f455

Please sign in to comment.