Skip to content

Commit 73fffdf

Browse files
committed
git commit -m "fix: prevent XSS in labels; add safe/HTML APIs and option
1 parent 955d7a4 commit 73fffdf

File tree

4 files changed

+50
-7
lines changed

4 files changed

+50
-7
lines changed

src/css-label.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,61 @@ export class CssLabel {
2323
private _customOpacity: number | undefined = undefined
2424
private _shouldBeShown = false
2525
private _text: string | number = ''
26+
/**
27+
* Tracks whether content was set via `dangerouslySetHtml` (`true`) or `setText` (`false`).
28+
* Needed so that switching mode with the same string (e.g. `setText` then `dangerouslySetHtml`) still updates the DOM.
29+
*/
30+
private _contentIsHtml = false
2631
private _customPadding: Padding | undefined = undefined
2732

2833
private _customPointerEvents: Options['pointerEvents'] | undefined
2934
private _customStyle: string | undefined
3035
private _customClassName: string | undefined
3136

32-
public constructor (container: HTMLDivElement, text?: string | number, dontInjectStyles?: boolean) {
37+
/**
38+
* @param container - The parent element for the label.
39+
* @param text - Initial label content (plain text or, if dangerousHtml is true, HTML).
40+
* @param dontInjectStyles - When true, global styles are not injected.
41+
* @param dangerousHtml - When true, text is set via innerHTML (XSS risk). Only use with trusted/sanitized content.
42+
*/
43+
public constructor (container: HTMLDivElement, text?: string | number, dontInjectStyles?: boolean, dangerousHtml?: boolean) {
3344
if (!dontInjectStyles && !globalCssLabelStyles) globalCssLabelStyles = injectStyles(cssLabelStyles)
3445
this._container = container
3546
this._updateClasses()
36-
if (text !== undefined) this.setText(text)
47+
if (text !== undefined) {
48+
if (dangerousHtml) {
49+
this.dangerouslySetHtml(text)
50+
} else {
51+
this.setText(text)
52+
}
53+
}
3754
this.resetFontSize()
3855
this.resetPadding()
3956
}
4057

4158
/**
42-
* Sets the text of the element.
59+
* Sets the text of the element using textContent (safe from XSS).
4360
* @param text - The text to set.
4461
*/
4562
public setText (text: string | number): void {
46-
if (this._text !== text) {
63+
if (this._text !== text || this._contentIsHtml) {
4764
this._text = text
48-
this.element.innerHTML = typeof text === 'number' ? String(text) : text
65+
this._contentIsHtml = false
66+
this.element.textContent = typeof text === 'number' ? String(text) : text
67+
this._needsMeasureUpdate = true
68+
}
69+
}
70+
71+
/**
72+
* Sets the inner HTML of the element. Only use with trusted or sanitized content.
73+
* WARNING: XSS risk — do not pass user-provided or unsanitized HTML.
74+
* @param html - The HTML to set (string or number; numbers are converted to string).
75+
*/
76+
public dangerouslySetHtml (html: string | number): void {
77+
if (this._text !== html || !this._contentIsHtml) {
78+
this._text = html
79+
this._contentIsHtml = true
80+
this.element.innerHTML = typeof html === 'number' ? String(html) : html
4981
this._needsMeasureUpdate = true
5082
}
5183
}

src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class LabelRenderer {
1414
private _dontInjectStyles: boolean | undefined
1515
private _padding: Padding | undefined
1616
private _fontSize: number | undefined
17+
private _dangerousHtml = false
1718

1819
public constructor (container: HTMLDivElement, options?: Options) {
1920
if (!options?.dontInjectStyles && !globalCssLabelRendererStyles) globalCssLabelRendererStyles = injectStyles(cssLabelContainerStyles)
@@ -26,6 +27,7 @@ export class LabelRenderer {
2627
if (options?.dontInjectStyles) this._dontInjectStyles = options.dontInjectStyles
2728
if (options?.padding) this._padding = options.padding
2829
if (options?.fontSize) this._fontSize = options.fontSize
30+
if (options?.dangerousHtml) this._dangerousHtml = options.dangerousHtml
2931

3032
if (options?.dispatchWheelEventElement) {
3133
this._dispatchWheelEventElement = options.dispatchWheelEventElement
@@ -42,13 +44,17 @@ export class LabelRenderer {
4244
if (exists) {
4345
labelsToDelete.delete(label.id)
4446
} else {
45-
const cssLabel = new CssLabel(this._container, label.text, this._dontInjectStyles)
47+
const cssLabel = new CssLabel(this._container, label.text, this._dontInjectStyles, this._dangerousHtml)
4648
this._cssLabels.set(label.id, cssLabel)
4749
this._elementToData.set(cssLabel.element, label)
4850
}
4951
const labelToUpdate = this._cssLabels.get(label.id)
5052
if (labelToUpdate) {
51-
labelToUpdate.setText(text)
53+
if (this._dangerousHtml) {
54+
labelToUpdate.dangerouslySetHtml(text)
55+
} else {
56+
labelToUpdate.setText(text)
57+
}
5258
labelToUpdate.setPosition(x, y)
5359
if (style !== undefined) labelToUpdate.setStyle(style)
5460
if (weight !== undefined) labelToUpdate.setWeight(weight)

src/stories/quick-start/html-multiline-labels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export function playHtmlMultilineLabels (div: HTMLDivElement): void {
66
const renderer = new LabelRenderer(div, {
77
fontSize: 12,
88
padding: labelPadding,
9+
dangerousHtml: true,
910
})
1011
// Container is 200×400; all labels at x = 100, y = 60, 120, 180, 240, 300, 360
1112
renderer.setLabels([

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ export interface Options {
2929
dontInjectStyles?: boolean;
3030
padding?: Padding;
3131
fontSize?: number;
32+
/** When `true`, label text is set via `innerHTML` (`dangerouslySetHtml`).
33+
* Only enable with trusted/sanitized content — XSS risk.
34+
*/
35+
dangerousHtml?: boolean;
3236
}

0 commit comments

Comments
 (0)