From 5765c99aea0fcc5bf363adff1c0499fffb6a8b95 Mon Sep 17 00:00:00 2001 From: bluestreak Date: Thu, 8 Jan 2026 01:04:02 +0000 Subject: [PATCH 1/2] fix: fix XSS security vulnurability --- src/js/console/grid.js | 49 ++++++++++++++----------------------- src/js/console/quick-vis.ts | 5 ++-- src/utils/escapeHtml.ts | 11 +++++++++ 3 files changed, 32 insertions(+), 33 deletions(-) create mode 100644 src/utils/escapeHtml.ts diff --git a/src/js/console/grid.js b/src/js/console/grid.js index 7e42064c3..87f523a20 100644 --- a/src/js/console/grid.js +++ b/src/js/console/grid.js @@ -33,17 +33,6 @@ const hashString = (str) => { return new Uint32Array([hash])[0].toString(36) } -const escapeHtml = (text) => { - const map = { - "<": "<", - ">": ">", - '"': """, - "'": "'", - } - - return text.replace(/[<>"']/g, (m) => map[m]) -} - export function grid(rootElement, _paginationFn, id) { const defaults = { gridID: "qdb-grid", @@ -963,9 +952,9 @@ export function grid(rootElement, _paginationFn, id) { const hType = document.createElement("span") addClass(hType, "qg-header-type") if (c.type !== "ARRAY") { - hType.innerHTML = c.type.toLowerCase() + hType.textContent = c.type.toLowerCase() } else if (c.dim > 2) { - hType.innerHTML = + hType.textContent = c.type.toUpperCase() + "(" + c.elemType.toUpperCase() + @@ -973,16 +962,16 @@ export function grid(rootElement, _paginationFn, id) { c.dim + ")" } else { - let html = c.elemType.toLowerCase() + "[]" + let typeText = c.elemType.toLowerCase() + "[]" if (c.dim > 1) { - html += "[]" + typeText += "[]" } - hType.innerHTML = html + hType.textContent = typeText } const hName = document.createElement("span") addClass(hName, "qg-header-name") - hName.innerHTML = c.name + hName.textContent = c.name const hysteresis = document.createElement("div") addClass(hysteresis, "qg-col-resize-hysteresis") @@ -1106,14 +1095,12 @@ export function grid(rootElement, _paginationFn, id) { } function getArrayString(cellData) { - return escapeHtml( - JSON.stringify(cellData, (_, val) => { - if (Number.isInteger(val)) { - return val.toString() + ".0" - } - return val - }).replace(/"/g, ""), - ) + return JSON.stringify(cellData, (_, val) => { + if (Number.isInteger(val)) { + return val.toString() + ".0" + } + return val + }).replace(/"/g, "") } function getDisplayedCellValue(column, cellData, columnWidth = null) { @@ -1126,10 +1113,10 @@ export function grid(rootElement, _paginationFn, id) { if (!isArray) { if (containsPrecision) { return Number.isInteger(cellData) - ? escapeHtml(cellData.toString() + ".0") - : escapeHtml(cellData.toString()) + ? cellData.toString() + ".0" + : cellData.toString() } - return escapeHtml(cellData.toString()) + return cellData.toString() } const arrayString = getArrayString(cellData) @@ -1166,7 +1153,7 @@ export function grid(rootElement, _paginationFn, id) { if (cellData !== null) { const layoutEntry = getLayoutEntry() const columnWidth = layoutEntry.deviants[column.name] ?? null - cell.innerHTML = getDisplayedCellValue(column, cellData, columnWidth) + cell.textContent = getDisplayedCellValue(column, cellData, columnWidth) cell.classList.remove("qg-null") @@ -1174,7 +1161,7 @@ export function grid(rootElement, _paginationFn, id) { cell.classList.add("qg-arr") } } else { - cell.innerHTML = "null" + cell.textContent = "null" cell.classList.add("qg-null") } } @@ -1769,7 +1756,7 @@ export function grid(rootElement, _paginationFn, id) { } addClass(focusedCell, "qg-c-active-pulse") - let valueToCopy = focusedCell.innerHTML + let valueToCopy = focusedCell.textContent if (focusedCell.classList.contains("qg-arr")) { const rowIndex = focusedCell.parentElement.rowIndex diff --git a/src/js/console/quick-vis.ts b/src/js/console/quick-vis.ts index 7c28076da..0587e62ed 100644 --- a/src/js/console/quick-vis.ts +++ b/src/js/console/quick-vis.ts @@ -38,6 +38,7 @@ import eChartsMacarons from "./utils/macarons" import { arrayEquals } from "./array-equals" import { eventBus } from "../../modules/EventBus" import { EventType } from "../../modules/EventBus/types" +import { escapeHtml } from "../../utils/escapeHtml" import * as QuestDB from "../../utils/questdb" import { AnyIfEmpty } from "react-redux" import { request } from "http" @@ -290,12 +291,12 @@ export function quickVis( const x = [] const columns = data.columns for (let i = 0; i < columns.length; i++) { - x[i] = { text: columns[i].name, value: columns[i].name } + x[i] = { text: escapeHtml(columns[i].name), value: columns[i].name } } xAxisPicker.setData(x) yAxisPicker.setData(x) - yAxisPicker.set(x.slice(1).map((item) => item.text)) + yAxisPicker.set(x.slice(1).map((item) => item.value)) // stash query text so that we can use this later to server for chart column values query = data.query diff --git a/src/utils/escapeHtml.ts b/src/utils/escapeHtml.ts new file mode 100644 index 000000000..b98771587 --- /dev/null +++ b/src/utils/escapeHtml.ts @@ -0,0 +1,11 @@ +export const escapeHtml = (text: string): string => { + const map: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + } + + return text.replace(/[&<>"']/g, (m) => map[m]) +} From 489179c8e0538a54cbdd7405cf018077211a14e8 Mon Sep 17 00:00:00 2001 From: emrberk Date: Thu, 8 Jan 2026 12:30:13 +0300 Subject: [PATCH 2/2] remove table dropping on afterEach --- e2e/tests/console/aiAssistant.spec.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/e2e/tests/console/aiAssistant.spec.js b/e2e/tests/console/aiAssistant.spec.js index 8ebf26f5b..c641e624e 100644 --- a/e2e/tests/console/aiAssistant.spec.js +++ b/e2e/tests/console/aiAssistant.spec.js @@ -1274,17 +1274,21 @@ describe("ai assistant", () => { }) describe("explain schema", () => { - beforeEach(() => { - cy.loadConsoleWithAuth(false, getOpenAIConfiguredSettings()) + before(() => { + cy.loadConsoleWithAuth() cy.typeQuery( "CREATE TABLE IF NOT EXISTS test_trades (symbol SYMBOL, price DOUBLE, ts TIMESTAMP) TIMESTAMP(ts) PARTITION BY DAY WAL;", ) cy.clickRunQuery() + cy.refreshSchema() + }) + after(() => { + cy.loadConsoleWithAuth() + cy.dropTable("test_trades") }) - afterEach(() => { - cy.typeQuery("DROP TABLE IF EXISTS test_trades;") - cy.clickRunQuery() + beforeEach(() => { + cy.loadConsoleWithAuth(false, getOpenAIConfiguredSettings()) }) it("should show processing status and display valid schema explanation", () => {