Skip to content

fix: resolve CodeQL JavaScript code-scanning alerts in Studio#4386

Merged
robfrank merged 1 commit into
mainfrom
fix/codeql-js-studio-xss
May 28, 2026
Merged

fix: resolve CodeQL JavaScript code-scanning alerts in Studio#4386
robfrank merged 1 commit into
mainfrom
fix/codeql-js-studio-xss

Conversation

@robfrank

Copy link
Copy Markdown
Collaborator

Summary

Resolves the 8 live CodeQL JavaScript code-scanning alerts on main. All are in Studio application JS (studio/src/main/resources/static/); the latest CodeQL analysis at HEAD contains exactly these 8 results.

xss-through-dom

incomplete-sanitization

Note on the other ~31 "open" JS alerts

Those reference studio/src/main/resources/static/components/DataTables/** (deleted in #2260) or code that was refactored away. They are absent from the current analysis and only lingered open because the CodeQL analysis category was renamed (javascriptjavascript-typescript), which prevented GitHub's auto-close. They have been dismissed separately as stale.

Test plan

  • node --check passes on all three edited .js files
  • Pre-commit hooks pass
  • Manual Studio smoke test: database switch, backup confirm dialog, toast notifications, query flame-graph tooltips, record editor save, materialized-view refresh button, profiler saved-runs menu
  • CodeQL re-scan on merge confirms the 8 alerts close

Note: webpack does not process Studio application JS (it only copies vendor libs), so there is no bundling/build step for these files.

Fixes the 8 live CodeQL JS alerts on main, all in Studio application code:

xss-through-dom:
- studio-database.js: render current database name with .text() instead of .html()
- studio-utils.js: escape the database name fed into the backupDatabase() confirm dialog
- index.html: set toast title/message via textContent instead of innerHTML
- studio-database.js: serve flame-graph tooltips from an in-memory array keyed by
  index instead of round-tripping HTML through a data attribute and re-parsing it

incomplete-sanitization:
- studio-record-editor.js: escape backslash before the single quote when building
  SQL string literals
- studio-database.js / studio-profiler.js: replace inline onclick string interpolation
  with data attributes and delegated click handlers

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several security and robustness improvements across the web studio frontend. Key changes include mitigating potential XSS vulnerabilities by replacing raw HTML string concatenation with safe DOM APIs (such as .textContent and jQuery's .text()), escaping HTML values in database and profiler modules, refactoring flame graph tooltips to store HTML in memory rather than in DOM attributes, and replacing inline onclick handlers with jQuery event listeners. Additionally, SQL string escaping in the record editor was improved to correctly escape backslashes. There are no review comments to address, and the changes look solid.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@robfrank robfrank merged commit 3f6c078 into main May 28, 2026
24 of 29 checks passed
@robfrank robfrank deleted the fix/codeql-js-studio-xss branch May 28, 2026 11:32
@claude

claude Bot commented May 28, 2026

Copy link
Copy Markdown

Code Review

The direction of this PR is correct - moving from inline onclick string interpolation to data-* attributes with delegated event handlers is the right pattern, and the textContent/.text() changes cleanly close the direct DOM-injection vectors. A few issues need addressing before merge.


Bug 1 (Regression - Medium): escapeHtml() result used in API URL path

File: studio-database.js, backupDatabase() (~line 857 / 871)

let database = escapeHtml(getCurrentDatabase());   // HTML-encodes the name
...
url: "api/v1/command/" + database,                 // then sends it in a URL path

escapeHtml() is meant for DOM insertion. When a database name contains & it becomes &, which the server does not recognize - the API call silently targets the wrong or nonexistent database. The same pattern already exists in dropDatabase() and resetDatabase() (pre-existing); this PR replicates it into backupDatabase().

Fix: keep two variables - the raw name for the URL, and the escaped version only for any HTML rendering:

let database = getCurrentDatabase();
let databaseDisplay = escapeHtml(database);

Or, if the name needs percent-encoding for safety in a URL segment, use encodeURIComponent(database) for the URL and a separate escapeHtml(database) for display.


Bug 2 (Regression - Medium): textContent in toast body breaks intentional HTML formatting

File: index.html (~lines 127-128)

toastEl.querySelector('.toast-body').textContent = message || '';

At least two globalNotify call sites in studio-database.js intentionally pass HTML in the message argument:

  • ~line 1017: "Type <b>" + escapeHtml(typeName) + "</b>: " + rebuilt + " records walked, ..." (repartition complete)
  • ~lines 3973-3975: msg += "<br><small>File: " + escapeHtml(data.backupFile) + "</small>"; ... globalNotify("Backup", msg, ...) (backup success)

After this change, users see the literal characters <b>, </b>, <br>, <small> in the toast - raw tag text instead of formatted output.

Fix options:

  1. Keep .innerHTML for the body but audit all callers to ensure user-controlled data is wrapped in escapeHtml() first (the two callers above already do this).
  2. Replace <b>/<small>/<br> HTML with CSS classes in the callers, then use textContent safely everywhere.

Option 1 is lower risk since both affected call sites are already using escapeHtml() on user data; the tags themselves (<b>, <br>, <small>) are not user-controlled.


Pre-existing: null guard bypass in backupDatabase() (and siblings)

File: studio-database.js (~line 858)

function getCurrentDatabase() {
  let db = $(".searchable-db-select").first().attr("data-selected");
  return db != null && db !== "" ? db.trim() : null;  // returns null, not ""
}

let database = escapeHtml(getCurrentDatabase());  // escapeHtml(null) returns null
if (database == "") {                             // null == "" is false in JS

When no database is selected, getCurrentDatabase() returns null, escapeHtml(null) also returns null (per studio-utils.js), and null == "" is false (loose equality), so the guard is silently skipped. The URL becomes "api/v1/command/null".

This behavior predates the PR and also exists in dropDatabase/resetDatabase. Not introduced here, but worth a follow-up. A minimal fix: if (!database) instead of if (database == "").


Note: Residual .html(username) XSS not addressed by this PR

File: studio-database.js (~lines 461-463)

let username = data.user || globalUsername || 'unknown';
$("#queryUser").html(username);
$("#databaseUser").html(username);
$("#tsUser").html(username);

data.user comes from the server API response and is passed directly to jQuery .html(). A malicious or compromised server could inject live HTML here. Out of scope for this PR's stated goal (fixing 8 specific CodeQL alerts), but worth a follow-up ticket since these are exact same class of XSS as the ones being fixed. The fix is .text(username).


The rest looks good

  • data-flame-tip-idx + flameTips[] array: the security model is sound - all user-controlled strings are wrapped in escapeHtml() before being stored, and tip.html() renders the pre-escaped HTML correctly. The flameTips = [] reset at the start of each renderFlameGraph() ensures indices stay consistent with the current DOM.
  • studio-profiler.js event delegation: correct - escapeHtml() in the attribute, jQuery .attr() decodes entities before passing to profilerLoadRun, so the original filename is preserved.
  • studio-record-editor.js: the replace(/\\/g, "\\\\").replace(/'/g, "\\'") order is correct (backslashes first, then quotes).
  • displayMvHealth() event delegation: correct pattern, binding happens after container.html(html).

@robfrank robfrank added this to the 26.6.1 milestone May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant