VYPR
High severityOSV Advisory· Published Jan 28, 2026· Updated Jan 29, 2026

CVE-2026-1513

CVE-2026-1513

Description

billboard.js before 3.18.0 allows an attacker to execute malicious JavaScript due to improper sanitization during chart option binding.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
billboard.jsnpm
< 3.18.03.18.0

Affected products

1

Patches

1
49e079cdd466

fix(util): update sanitization function (#4085)

https://github.com/naver/billboard.jsJae Sung ParkJan 19, 2026via ghsa
7 files changed · +379 78
  • src/ChartInternal/internals/legend.ts+1 5 modified
    @@ -12,7 +12,6 @@ import {
     	isDefined,
     	isEmpty,
     	isFunction,
    -	notEmpty,
     	sanitize,
     	toMap,
     	tplProcess
    @@ -872,12 +871,9 @@ export default {
     
     		if (usePoint) {
     			const ids: string[] = [];
    +			const pattern = $$.getValidPointPattern();
     
     			l.append(d => {
    -				const pattern = notEmpty(config.point_pattern) ?
    -					config.point_pattern :
    -					[config.point_type];
    -
     				ids.indexOf(d) === -1 && ids.push(d);
     
     				let point = pattern[ids.indexOf(d) % pattern.length];
    
  • src/ChartInternal/shape/point.common.ts+19 1 modified
    @@ -67,6 +67,8 @@ export default {
     	 * @private
     	 */
     	hasValidPointType(type?: string): boolean {
    +		// For point.pattern, allow additional SVG shape tags (polygon, ellipse, use)
    +		// These will be sanitized before use
     		return /^(circle|rect(angle)?|polygon|ellipse|use)$/i.test(type || this.config.point_type);
     	},
     
    @@ -87,6 +89,22 @@ export default {
     		return `${datetimeId}-point${id}`;
     	},
     
    +	/**
    +	 * Get validated point pattern array
    +	 * @returns {Array} Array of point types
    +	 * @private
    +	 */
    +	getValidPointPattern(): string[] {
    +		const {config} = this;
    +
    +		// Ensure point_type is restricted to 'circle' or 'rectangle' only
    +		const validPointType = /^(circle|rect(angle)?)$/i.test(config.point_type) ?
    +			config.point_type :
    +			"circle";
    +
    +		return notEmpty(config.point_pattern) ? config.point_pattern : [validPointType];
    +	},
    +
     	/**
     	 * Get generate point function
     	 * @returns {function}
    @@ -96,7 +114,7 @@ export default {
     		const $$ = this;
     		const {$el, config} = $$;
     		const ids: string[] = [];
    -		const pattern = notEmpty(config.point_pattern) ? config.point_pattern : [config.point_type];
    +		const pattern = $$.getValidPointPattern();
     
     		return function(method, context, ...args) {
     			return function(d) {
    
  • src/config/Options/common/legend.ts+1 0 modified
    @@ -25,6 +25,7 @@ export default {
     	 *   - title {string}: data's id value
     	 *   - color {string}: color string
     	 *   - data {Array}: data array
    +	 *  - **NOTE:** While basic XSS sanitization is applied, if you're allowing user-provided chart options in a service exposed to other users, you should implement additional security measures to prevent sophisticated XSS attacks.
     	 * @property {string} [legend.position=bottom] Change the position of legend.<br>
     	 *  Available values are: `bottom`, `right` and `inset` are supported.
     	 * @property {object} [legend.inset={anchor: 'top-left',x: 10,y: 0,step: undefined}] Change inset legend attributes.<br>
    
  • src/config/Options/common/point.ts+1 0 modified
    @@ -59,6 +59,7 @@ export default {
     	 *   - This is an `experimental` feature and can have some unexpected behaviors.
     	 *   - If chart has 'bubble' type, only circle can be used.
     	 *   - For IE, non circle point expansions are not supported due to lack of transform support.
    +	 *   - While basic XSS sanitization is applied, if you're allowing user-provided chart options in a service exposed to other users, you should implement additional security measures to prevent sophisticated XSS attacks.
     	 * - **Available Values:**
     	 *   - circle
     	 *   - rectangle
    
  • src/config/Options/common/tooltip.ts+1 0 modified
    @@ -58,6 +58,7 @@ export default {
     	 *    - **{=COLOR}**: data color.
     	 *    - **{=NAME}**: data id value.
     	 *    - **{=VALUE}**: data value.
    +	 *  - **NOTE:** While basic XSS sanitization is applied, if you're allowing user-provided chart options in a service exposed to other users, you should implement additional security measures to prevent sophisticated XSS attacks.
     	 * @property {object} [tooltip.contents.text=undefined] Set additional text content within data loop, using template syntax.
     	 *  - **NOTE:** It should contain `{ key: Array, ... }` value
     	 *    - 'key' name is used as substitution within template as '{=KEY}'
    
  • src/module/util.ts+162 72 modified
    @@ -8,6 +8,148 @@ import {pointer as d3Pointer} from "d3-selection";
     import type {d3Selection} from "../../types/types";
     import {document, requestAnimationFrame, window} from "./browser";
     
    +// ====================================
    +// Internal Helper (Not Exported)
    +// ====================================
    +
    +/**
    + * Get boundingClientRect or BBox with caching.
    + * Internal helper for getBoundingRect() and getBBox()
    + * @param {boolean} relativeViewport Relative to viewport - true: will use .getBoundingClientRect(), false: will use .getBBox()
    + * @param {SVGElement} node Target element
    + * @param {boolean} forceEval Force evaluation
    + * @returns {object}
    + * @private
    + */
    +function _getRect(
    +	relativeViewport: boolean,
    +	node: SVGElement & Partial<{rect: DOMRect | SVGRect}>,
    +	forceEval = false
    +): DOMRect | SVGRect {
    +	const _ = n => n[relativeViewport ? "getBoundingClientRect" : "getBBox"]();
    +
    +	if (forceEval) {
    +		return _(node);
    +	} else {
    +		// will cache the value if the element is not a SVGElement or the width is not set
    +		const needEvaluate = !("rect" in node) || (
    +			"rect" in node && node.hasAttribute("width") &&
    +			node.rect!.width !== +(node.getAttribute("width") || 0)
    +		);
    +
    +		return needEvaluate ? (node.rect = _(node)) : node.rect!;
    +	}
    +}
    +
    +/**
    + * Internal helper to iterate over array items and invoke a callback for each valid item
    + * @param {Array} items Array to iterate
    + * @param {function} callback Callback function (item, index) => void
    + * @private
    + */
    +function _forEachValidItem<T>(items: T[], callback: (item: T, index: number) => void): void {
    +	for (let i = 0; i < items.length; i++) {
    +		const item = items[i];
    +
    +		if (item) {
    +			callback(item, i);
    +		}
    +	}
    +}
    +
    +/**
    + * Private sanitization utilities
    + * Encapsulates all XSS prevention patterns and helper functions
    + * @private
    + */
    +const _sanitize = (() => {
    +	const DANGEROUS_TAGS =
    +		"script|iframe|object|embed|form|input|button|textarea|select|style|link|meta|base|math|isindex";
    +
    +	// HTML entity character code map (for decoding)
    +	const ENTITY_MAP = {
    +		quot: 34,
    +		amp: 38,
    +		apos: 39,
    +		lt: 60,
    +		gt: 62,
    +		nbsp: 160,
    +		iexcl: 161,
    +		cent: 162,
    +		pound: 163,
    +		curren: 164,
    +		yen: 165
    +	};
    +
    +	// Angle bracket codes (< and >) - never decode these to prevent tag bypass
    +	const LT_CODE = 60;
    +	const GT_CODE = 62;
    +
    +	// Maximum sanitization iterations to prevent infinite loops
    +	const MAX_ITERATIONS = 10;
    +
    +	// Regular expressions (compiled once for performance)
    +	const rx = {
    +		tags: new RegExp(
    +			`<(${DANGEROUS_TAGS})\\b[\\s\\S]*?>([\\s\\S]*?<\\/(${DANGEROUS_TAGS})\\s*>)?`,
    +			"gi"
    +		),
    +		htmlEntity: /&#x([0-9a-f]+);?|&#([0-9]+);?|&([a-z]+);/gi,
    +		// eslint-disable-next-line no-control-regex
    +		controlChar: /[\x00-\x1F\x7F]/g,
    +		eventHandler:
    +			/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|`[^`]*`|[^\s>]*)|on\w+\s*=\s*(?:"[^"]*"|'[^']*'|`[^`]*`|[^\s>]*)/gi,
    +		dangerousUri: /(href|src|action|xlink:href)\s*=\s*(['"`]?)([^'"`>\s]*)\2/gi,
    +		dangerousProtocol: /^(javascript|data|vbscript):/
    +	};
    +
    +	return {
    +		ENTITY_MAP,
    +		LT_CODE,
    +		GT_CODE,
    +		MAX_ITERATIONS,
    +		rx,
    +
    +		/**
    +		 * Decode HTML entities to prevent bypass attacks
    +		 * @param {string} str String with potential HTML entities
    +		 * @returns {string} Decoded string
    +		 */
    +		decodeEntities(str: string): string {
    +			return str.replace(rx.htmlEntity, (match, hex, dec, named) => {
    +				const code = hex ?
    +					parseInt(hex, 16) :
    +					dec ?
    +					parseInt(dec, 10) :
    +					named ?
    +					ENTITY_MAP[named.toLowerCase()] || 0 :
    +					0;
    +
    +				// Never decode angle brackets to prevent tag bypass
    +				return code && code !== LT_CODE && code !== GT_CODE ?
    +					String.fromCharCode(code) :
    +					match;
    +			});
    +		},
    +
    +		/**
    +		 * Remove dangerous URI protocols from attribute values
    +		 * @param {string} str String to sanitize
    +		 * @returns {string} Sanitized string
    +		 */
    +		removeDangerousUris(str: string): string {
    +			return str.replace(rx.dangerousUri, (match, attr, quote, value) => {
    +				const normalized = value.toLowerCase().replace(/\s/g, "");
    +				return rx.dangerousProtocol.test(normalized) ? `${attr}=${quote}${quote}` : match;
    +			});
    +		}
    +	};
    +})();
    +
    +// ====================================
    +// Exported
    +// ====================================
    +
     const isValue = (v: any): boolean => v || v === 0;
     const isFunction = (v: unknown): v is (...args: any[]) => any => typeof v === "function";
     const isString = (v: unknown): v is string => typeof v === "string";
    @@ -116,42 +258,43 @@ function endall(transition, cb: Function): void {
     	}
     }
     
    -// Sanitize patterns (blacklist approach with repeated application)
    -const DANGEROUS_TAGS =
    -	"script|iframe|object|embed|form|input|button|textarea|select|style|link|meta|base|math|isindex";
    -const sanitizeRx = {
    -	tags: new RegExp(
    -		`<(${DANGEROUS_TAGS})\\b[\\s\\S]*?>([\\s\\S]*?<\\/(${DANGEROUS_TAGS})\\s*>)?`,
    -		"gi"
    -	),
    -	// Handles: whitespace, slash, quotes before event handlers (e.g., <img/onerror=...>, <img src="x"onerror=...>)
    -	eventHandlers: /[\s/"']+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi,
    -	// Handles: javascript/data/vbscript URIs with optional whitespace/newlines between protocol and colon
    -	dangerousURIs: /(href|src|action|xlink:href)\s*=\s*["']?\s*(javascript|data|vbscript)\s*:/gi
    -};
    -
     /**
      * Sanitize HTML string to prevent XSS attacks
      * Uses blacklist approach with repeated application to prevent nested tag bypass
    + * Handles encoded characters (HTML entities, Unicode escapes) to prevent bypass attacks
      * @param {string} str Target string value
      * @returns {string} Sanitized string with dangerous elements removed
      * @private
      */
     function sanitize(str: string): string {
    +	// Early return for non-string, empty string, or string without HTML
     	if (!isString(str) || !str || str.indexOf("<") === -1) {
     		return str;
     	}
     
     	let result = str;
     	let prev: string;
    +	let iterations = 0;
     
     	// Repeat until no more changes (prevents nested tag attacks like <scri<script>pt>)
     	do {
     		prev = result;
    -		result = result
    -			.replace(sanitizeRx.tags, "")
    -			.replace(sanitizeRx.eventHandlers, "")
    -			.replace(sanitizeRx.dangerousURIs, "$1=\"\"");
    +
    +		// 1. Decode HTML entities to prevent bypass (e.g., jav&#x0A;ascript:)
    +		// 2. Remove control characters (NULL, tab, newline, etc.)
    +		// 3. Remove dangerous tags (script, iframe, etc.)
    +		result = _sanitize.decodeEntities(result)
    +			.replace(_sanitize.rx.controlChar, "")
    +			.replace(_sanitize.rx.tags, "")
    +			.replace(_sanitize.rx.eventHandler, ""); // 4. Remove event handlers
    +
    +		// 5. Remove dangerous URIs (javascript:, data:, vbscript:)
    +		result = _sanitize.removeDangerousUris(result);
    +
    +		// Safety check to prevent infinite loops
    +		if (++iterations >= _sanitize.MAX_ITERATIONS) {
    +			break;
    +		}
     	} while (result !== prev);
     
     	return result;
    @@ -250,7 +393,7 @@ function getPathBox(
      * @returns {Array} [x, y] Coordinates x, y array
      * @private
      */
    -function getPointer(event, element?: SVGElement): number[] {
    +function getPointer(event, element?: HTMLElement | SVGElement): number[] {
     	const touches = event &&
     		(event.touches || (event.sourceEvent && event.sourceEvent.touches))?.[0];
     	let pointer = [0, 0];
    @@ -284,59 +427,6 @@ function getBrushSelection(ctx) {
     	return selection;
     }
     
    -// ====================================
    -// Internal Helper (Not Exported)
    -// ====================================
    -
    -/**
    - * Get boundingClientRect or BBox with caching.
    - * Internal helper for getBoundingRect() and getBBox()
    - * @param {boolean} relativeViewport Relative to viewport - true: will use .getBoundingClientRect(), false: will use .getBBox()
    - * @param {SVGElement} node Target element
    - * @param {boolean} forceEval Force evaluation
    - * @returns {object}
    - * @private
    - */
    -function _getRect(
    -	relativeViewport: boolean,
    -	node: SVGElement & Partial<{rect: DOMRect | SVGRect}>,
    -	forceEval = false
    -): DOMRect | SVGRect {
    -	const _ = n => n[relativeViewport ? "getBoundingClientRect" : "getBBox"]();
    -
    -	if (forceEval) {
    -		return _(node);
    -	} else {
    -		// will cache the value if the element is not a SVGElement or the width is not set
    -		const needEvaluate = !("rect" in node) || (
    -			"rect" in node && node.hasAttribute("width") &&
    -			node.rect!.width !== +(node.getAttribute("width") || 0)
    -		);
    -
    -		return needEvaluate ? (node.rect = _(node)) : node.rect!;
    -	}
    -}
    -
    -/**
    - * Internal helper to iterate over array items and invoke a callback for each valid item
    - * @param {Array} items Array to iterate
    - * @param {function} callback Callback function (item, index) => void
    - * @private
    - */
    -function _forEachValidItem<T>(items: T[], callback: (item: T, index: number) => void): void {
    -	for (let i = 0; i < items.length; i++) {
    -		const item = items[i];
    -
    -		if (item) {
    -			callback(item, i);
    -		}
    -	}
    -}
    -
    -// ====================================
    -// Exported
    -// ====================================
    -
     /**
      * Get boundingClientRect.
      * @param {SVGElement} node Target element
    
  • test/internals/util-spec.ts+194 0 modified
    @@ -230,6 +230,200 @@ describe("UTIL", function() {
     			// Multiple levels of nesting
     			expect(sanitize("<scr<scr<script></script>ipt></script>ipt>alert(3)</script>")).to.not.include("script");
     		});
    +
    +		it("should handle HTML entity encoded attacks", () => {
    +			// Newline in javascript: URL
    +			expect(sanitize("<a href='jav&#x0A;ascript:alert(1)'>click</a>")).to.not.include("javascript:");
    +
    +			// NULL byte in javascript: URL
    +			expect(sanitize("<a href='java&#x00;script:alert(1)'>click</a>")).to.not.include("javascript:");
    +
    +			// Tab character
    +			expect(sanitize("<a href='java&#x09;script:alert(1)'>click</a>")).to.not.include("javascript:");
    +
    +			// Fully encoded javascript:
    +			expect(sanitize("<a href='&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;alert(1)'>click</a>")).to.not.include("javascript:");
    +
    +			// Hex encoded entities
    +			expect(sanitize("<a href='&#x6A;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3A;alert(1)'>click</a>")).to.not.include("javascript:");
    +
    +			// Mixed encoding
    +			expect(sanitize("<a href='jav&#97;scr&#105;pt:alert(1)'>click</a>")).to.not.include("javascript:");
    +
    +			// Hex encoding with single character (&#x76; = 'v')
    +			expect(sanitize("<a href='ja&#x76;ascript:alert(123)'>click</a>")).to.not.include("javascript:");
    +
    +			// Decimal encoding with padding zeros (&#0000118; = 'v')
    +			expect(sanitize("<a href='ja&#0000118;ascript:alert(123)'>click</a>")).to.not.include("javascript:");
    +
    +			// Decimal encoding (&#118; = 'v')
    +			expect(sanitize("<a href='ja&#118;ascript:alert(123)'>click</a>")).to.not.include("javascript:");
    +		});
    +
    +		it("should handle control character injection", () => {
    +			// NULL byte
    +			expect(sanitize("<script\x00>alert(1)</script>")).to.not.include("script");
    +
    +			// Tab character in tag
    +			expect(sanitize("<script\t>alert(1)</script>")).to.not.include("script");
    +
    +			// Newline in tag
    +			expect(sanitize("<script\n>alert(1)</script>")).to.not.include("script");
    +
    +			// Carriage return
    +			expect(sanitize("<script\r>alert(1)</script>")).to.not.include("script");
    +
    +			// Carriage return + newline in URL (testing \r\n in javascript:)
    +			expect(sanitize("<a href='jav\r\nascript:alert(123)'>click</a>")).to.not.include("javascript:");
    +		});
    +
    +		it("should handle various quote styles in event handlers", () => {
    +			// Double quotes
    +			expect(sanitize("<img src=x onerror=\"alert(1)\">")).to.not.include("onerror");
    +
    +			// Single quotes
    +			expect(sanitize("<img src=x onerror='alert(1)'>")).to.not.include("onerror");
    +
    +			// Backticks
    +			expect(sanitize("<img src=x onerror=`alert(1)`>")).to.not.include("onerror");
    +
    +			// No quotes
    +			expect(sanitize("<img src=x onerror=alert(1)>")).to.not.include("onerror");
    +
    +			// Space before attribute
    +			expect(sanitize("<img src=x  onerror=alert(1)>")).to.not.include("onerror");
    +		});
    +
    +		it("should handle data: URI attacks", () => {
    +			// HTML in data URI - the script tag is removed
    +			const result1 = sanitize("<a href='data:text/html,<script>alert(1)</script>'>click</a>");
    +			expect(result1).to.not.include("script");
    +			expect(result1).to.not.include("data:");
    +
    +			// Base64 encoded data URI - data: protocol is removed
    +			expect(sanitize("<a href='data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=='>click</a>")).to.not.include("data:");
    +
    +			// Data URI with SVG - the onload event handler is removed, dangerous tags are removed
    +			const svgResult = sanitize("<img src='data:image/svg+xml,<svg onload=alert(1)>'>");
    +			expect(svgResult).to.not.include("onload");
    +			// Note: data: in attribute values that don't contain dangerous content may remain
    +		});
    +
    +		it("should handle vbscript: protocol attacks", () => {
    +			expect(sanitize("<a href='vbscript:msgbox(1)'>click</a>")).to.not.include("vbscript:");
    +			expect(sanitize("<a href='VBScript:MsgBox(1)'>click</a>")).to.not.include("vbscript:");
    +			expect(sanitize("<img src='vbscript:msgbox(1)'>")).to.not.include("vbscript:");
    +		});
    +
    +		it("should handle SVG-based XSS attacks", () => {
    +			// SVG with onload
    +			expect(sanitize("<svg onload='alert(1)'></svg>")).to.not.include("onload");
    +
    +			// SVG with nested script
    +			expect(sanitize("<svg><script>alert(1)</script></svg>")).to.not.include("script");
    +
    +			// SVG with animate and set
    +			const result = sanitize("<svg><animate onbegin='alert(1)'/></svg>");
    +			expect(result).to.not.include("onbegin");
    +		});
    +
    +		it("should handle case variations", () => {
    +			// Mixed case script tag
    +			expect(sanitize("<ScRiPt>alert(1)</sCrIpT>")).to.not.include("script");
    +
    +			// Mixed case event handler
    +			expect(sanitize("<img src=x OnErRoR=alert(1)>")).to.not.include("onerror");
    +
    +			// Mixed case protocol
    +			expect(sanitize("<a href='JaVaScRiPt:alert(1)'>click</a>")).to.not.include("javascript:");
    +		});
    +
    +		it("should handle whitespace variations", () => {
    +			// Space in tag name - "scr ipt" is not recognized as "script" tag
    +			// The regex only matches <script> as complete tag name, so this passes through
    +			// But the closing </script> tag is recognized and removed
    +			const result = sanitize("<scr ipt>alert(1)</script>");
    +			// Since the opening tag isn't recognized as script, content may remain
    +			expect(result).to.include("alert(1)");
    +
    +			// Newlines around equals
    +			expect(sanitize("<img src=x\nonerror\n=\nalert(1)>")).to.not.include("onerror");
    +
    +			// Multiple spaces
    +			expect(sanitize("<img    src=x    onerror=alert(1)>")).to.not.include("onerror");
    +		});
    +
    +		it("should handle obfuscated attacks", () => {
    +			// Nested encoding
    +			expect(sanitize("<scri<script>pt>alert(1)</scri</script>pt>")).to.not.include("script");
    +
    +			// Multiple nested levels
    +			expect(sanitize("<scr<scr<script>x</script>ipt>y</script>ipt>alert(1)</script>")).to.not.include("script");
    +
    +			// Mixed nesting with different tags
    +			expect(sanitize("<scr<iframe></iframe>ipt>alert(1)</script>")).to.not.include("script");
    +		});
    +
    +		it("should preserve safe content", () => {
    +			// Normal URLs
    +			expect(sanitize("<a href='https://example.com'>link</a>")).to.include("https://example.com");
    +			expect(sanitize("<a href='http://example.com'>link</a>")).to.include("http://example.com");
    +			expect(sanitize("<a href='/path/to/page'>link</a>")).to.include("/path/to/page");
    +			expect(sanitize("<a href='#section'>link</a>")).to.include("#section");
    +
    +			// Safe protocols
    +			expect(sanitize("<a href='mailto:test@example.com'>email</a>")).to.include("mailto:");
    +			expect(sanitize("<a href='tel:+1234567890'>call</a>")).to.include("tel:");
    +
    +			// Normal HTML with classes and attributes
    +			expect(sanitize("<div class='container' id='main'>content</div>")).to.be.equal("<div class='container' id='main'>content</div>");
    +			expect(sanitize("<span style='color:red'>text</span>")).to.be.equal("<span style='color:red'>text</span>");
    +		});
    +
    +		it("should handle edge cases", () => {
    +			// Empty attributes
    +			expect(sanitize("<a href=''>empty link</a>")).to.be.equal("<a href=''>empty link</a>");
    +
    +			// Multiple dangerous elements
    +			const input = "<script>x</script><iframe></iframe><object></object>";
    +			const result = sanitize(input);
    +			expect(result).to.not.include("script");
    +			expect(result).to.not.include("iframe");
    +			expect(result).to.not.include("object");
    +
    +			// Mixed safe and dangerous
    +			const mixed = "<div>safe</div><script>dangerous</script><p>also safe</p>";
    +			const cleaned = sanitize(mixed);
    +			expect(cleaned).to.include("<div>safe</div>");
    +			expect(cleaned).to.include("<p>also safe</p>");
    +			expect(cleaned).to.not.include("script");
    +		});
    +
    +		it("should handle real-world attack patterns", () => {
    +			// PortSwigger XSS patterns
    +			expect(sanitize("<img src=1 onerror=alert(1)>")).to.not.include("onerror");
    +			expect(sanitize("<svg><animatetransform onbegin=alert(1)>")).to.not.include("onbegin");
    +
    +			// OWASP patterns with entity encoding
    +			expect(sanitize("<IMG SRC=j&#X41vascript:alert('test')>")).to.not.include("javascript:");
    +
    +			// Backtick quoted attributes - known limitation
    +			// Current implementation handles quotes and double-quotes but backticks in attributes are edge case
    +			const backtickResult = sanitize("<IMG SRC=`javascript:alert('XSS')`>");
    +			// We verify that at minimum the structure is preserved (not broken)
    +			expect(backtickResult).to.include("IMG");
    +
    +			// Mutation XSS (mXSS) - event handlers are removed
    +			expect(sanitize("<noscript><p title=\"</noscript><img src=x onerror=alert(1)>\">")).to.not.include("onerror");
    +
    +			// Polyglot XSS - complex attack string with multiple vectors
    +			const polyglotResult = sanitize("javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/\"/+/onmouseover=1/+/[*/[]/+alert(1)//'>");
    +			// Event handlers should be removed
    +			expect(polyglotResult).to.not.include("onload=");
    +			expect(polyglotResult).to.not.include("onmouseover=");
    +			// The main dangerous content (event handlers) is removed, making the payload harmless
    +			// Note: Closing tags without opening tags may remain but are harmless in HTML
    +		});
     	});
     
     	describe("parseDate", () => {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.