This is an automated email from the ASF dual-hosted git repository.
pawarprasad123 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/master by this push:
new 1abb1367f ATLAS-5370: Atlas UI : Encode text in D3 graph tooltips
Lineage and Profile charts (#722)
1abb1367f is described below
commit 1abb1367fdca836c6600028e3e629ead48310c95
Author: Prasad Pawar <[email protected]>
AuthorDate: Mon Aug 10 19:05:50 2026 +0530
ATLAS-5370: Atlas UI : Encode text in D3 graph tooltips Lineage and Profile
charts (#722)
* Atlas UI : Encode text in D3 graph tooltips Lineage and Profile charts
* ATLAS-5370: Atlas UI : Encode text in D3 graph tooltips Lineage and
Profile charts
---
.../src/Utils/__tests__/escapeHtml.test.ts | 258 +++++++++++++++++++++
.../views/Lineage/atlas-lineage/src/Utils/index.js | 10 +-
.../src/views/Lineage/atlas-lineage/src/index.js | 12 +-
.../js/external_lib/atlas-lineage/dist/index.js | 2 +-
.../js/external_lib/atlas-lineage/src/index.js | 9 +-
.../external_lib/atlas-lineage/webpack.config.js | 3 +-
dashboardv2/public/js/main.js | 2 +-
dashboardv2/public/js/migration.js | 2 +-
.../public/js/views/graph/ProfileBarChart.js | 17 +-
9 files changed, 299 insertions(+), 16 deletions(-)
diff --git
a/dashboard/src/views/Lineage/atlas-lineage/src/Utils/__tests__/escapeHtml.test.ts
b/dashboard/src/views/Lineage/atlas-lineage/src/Utils/__tests__/escapeHtml.test.ts
new file mode 100644
index 000000000..6c55f0fc0
--- /dev/null
+++
b/dashboard/src/views/Lineage/atlas-lineage/src/Utils/__tests__/escapeHtml.test.ts
@@ -0,0 +1,258 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+jest.mock("../LineageUtils", () => ({ default: {} }));
+jest.mock("../DataUtils", () => ({ default: {} }));
+
+import { escapeHtml } from "../index";
+
+/**
+ * Mirrors underscore _.escape used in classic UI:
+ * - atlas-lineage (dashboardv2/external_lib)
+ * - ProfileBarChart.js
+ */
+const underscoreEscape = (value: unknown): string => {
+ if (value == null) {
+ return "";
+ }
+
+ const escapeMap: Record<string, string> = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+ "`": "`"
+ };
+
+ return String(value).replace(/[&<>"'`]/g, (match) => escapeMap[match]);
+};
+
+const expectSafeTooltipText = (encoded: string) => {
+ expect(encoded).not.toMatch(/<[^&!]/);
+ expect(encoded).not.toContain("<script");
+ expect(encoded).not.toContain("<img");
+};
+
+describe("lineage tooltip escapeHtml (React)", () => {
+ describe("HTML metacharacter encoding", () => {
+ it("escapes angle brackets in malicious tooltip labels", () => {
+ const payload = '<img src=x onerror="window.__xss=1">';
+ const escaped = escapeHtml(payload);
+
+ expect(escaped).not.toContain("<img");
+ expect(escaped).toContain("<img");
+ expect(escaped).toContain(""");
+ expectSafeTooltipText(escaped);
+ });
+
+ it("escapes script tags", () => {
+ const escaped =
escapeHtml('<script>alert("x")</script>');
+
expect(escaped).toBe("<script>alert("x")</script>");
+ expectSafeTooltipText(escaped);
+ });
+
+ it("escapes ampersand", () => {
+ expect(escapeHtml("a & b")).toBe("a & b");
+ });
+
+ it("double-encodes already-encoded ampersand entities safely",
() => {
+ expect(escapeHtml("a & b")).toBe("a &amp; b");
+ expectSafeTooltipText(escapeHtml("a & b"));
+ });
+
+ it("escapes single quotes", () => {
+ expect(escapeHtml("O'Brien")).toBe("O'Brien");
+ });
+
+ it("escapes double quotes", () => {
+ expect(escapeHtml('say "hello"')).toBe("say
"hello"");
+ });
+
+ it("escapes all special characters in one payload", () => {
+ const payload = `<a href='x'>&"test"`;
+ const escaped = escapeHtml(payload);
+ expect(escaped).toBe("<a
href='x'>&"test"");
+ expectSafeTooltipText(escaped);
+ });
+ });
+
+ describe("nullish and empty inputs", () => {
+ it("handles null and undefined values", () => {
+ expect(escapeHtml(null)).toBe("");
+ expect(escapeHtml(undefined)).toBe("");
+ });
+
+ it("handles empty string", () => {
+ expect(escapeHtml("")).toBe("");
+ });
+
+ it("handles whitespace-only label used when displayText is
missing", () => {
+ expect(escapeHtml(" ")).toBe(" ");
+ });
+ });
+
+ describe("numeric and coerced values", () => {
+ it("coerces numeric values to string", () => {
+ expect(escapeHtml(123)).toBe("123");
+ expect(escapeHtml(0)).toBe("0");
+ });
+
+ it("coerces boolean values to string", () => {
+ expect(escapeHtml(true)).toBe("true");
+ expect(escapeHtml(false)).toBe("false");
+ });
+ });
+
+ describe("plain text preservation", () => {
+ it("preserves safe entity display names", () => {
+
expect(escapeHtml("sales_db.customers")).toBe("sales_db.customers");
+ });
+
+ it("preserves qualified names with dots and underscores", () =>
{
+
expect(escapeHtml("hive.db.table_v1")).toBe("hive.db.table_v1");
+ });
+
+ it("preserves unicode characters in entity names", () => {
+ expect(escapeHtml("データ_テーブル")).toBe("データ_テーブル");
+ });
+
+ it("preserves newlines in multiline query text", () => {
+ const queryText = "SELECT *\nFROM t\nWHERE id = 1";
+ expect(escapeHtml(queryText)).toBe(queryText);
+ expectSafeTooltipText(escapeHtml(queryText));
+ });
+ });
+
+ describe("lineage tooltip field scenarios", () => {
+ it("encodes toolTipLabel from entity displayText", () => {
+ const label = 'db.table<script>alert(1)</script>';
+ const escaped = escapeHtml(label);
+
expect(escaped).toBe("db.table<script>alert(1)</script>");
+ expectSafeTooltipText(escaped);
+ });
+
+ it("encodes typeName values", () => {
+ const escaped = escapeHtml("hive_table<script>");
+ expect(escaped).toBe("hive_table<script>");
+ expectSafeTooltipText(escaped);
+ });
+
+ it("encodes queryText with SQL quotes and comparison
operators", () => {
+ const queryText =
+ "SELECT * FROM t WHERE x < 1 AND name = \"foo\"
AND y > 0";
+ const escaped = escapeHtml(queryText);
+
+ expect(escaped).toContain("<");
+ expect(escaped).toContain(">");
+ expect(escaped).toContain(""");
+ expectSafeTooltipText(escaped);
+ });
+
+ it("encodes static toolTipTitle values", () => {
+ expect(escapeHtml("Type")).toBe("Type");
+
expect(escapeHtml('Type<script>')).toBe("Type<script>");
+ });
+ });
+
+ describe("intentional differences from classic _.escape", () => {
+ it("does not encode backtick in text content (safe inside span
tags)", () => {
+ expect(escapeHtml("db`table")).toBe("db`table");
+ expectSafeTooltipText(escapeHtml("db`table"));
+ });
+ });
+});
+
+describe("classic UI tooltip encoding (underscore _.escape parity)", () => {
+ const tooltipPayloads = [
+ '<img src=x onerror="alert(1)">',
+ "O'Brien",
+ "a & b",
+ "sales_db.customers",
+ 123,
+ "",
+ null,
+ undefined
+ ];
+
+ it.each(tooltipPayloads)(
+ "underscoreEscape produces safe output for %p",
+ (payload) => {
+ const escaped = underscoreEscape(payload);
+
+ if (payload == null) {
+ expect(escaped).toBe("");
+ return;
+ }
+
+ expectSafeTooltipText(escaped);
+
+ const raw = String(payload);
+ if (/[&<>"'`]/.test(raw)) {
+ expect(escaped).not.toBe(raw);
+ } else {
+ expect(escaped).toBe(raw);
+ }
+ }
+ );
+
+ it("handles null and undefined like production _.escape", () => {
+ expect(underscoreEscape(null)).toBe("");
+ expect(underscoreEscape(undefined)).toBe("");
+ });
+
+ it("preserves numeric count values for profile bar chart tooltips", ()
=> {
+ expect(underscoreEscape(42)).toBe("42");
+ expect(underscoreEscape(0)).toBe("0");
+ });
+
+ it("encodes lineage queryText with quotes and angle brackets", () => {
+ const queryText = 'SELECT * FROM t WHERE x < 1 AND name =
"foo"';
+ const escaped = underscoreEscape(queryText);
+
+ expect(escaped).toContain("<");
+ expect(escaped).toContain(""");
+ expectSafeTooltipText(escaped);
+ });
+
+ it("encodes lineage typeName values", () => {
+ const escaped = underscoreEscape("hive_table<script>");
+ expect(escaped).toBe("hive_table<script>");
+ expectSafeTooltipText(escaped);
+ });
+
+ it("encodes backtick characters used in entity names", () => {
+ expect(underscoreEscape("db`table")).toBe("db`table");
+ });
+
+ it("react escapeHtml and classic _.escape both produce safe tooltip
output", () => {
+ const payloads = [
+ '<img src=x onerror="alert(1)">',
+ '<script>alert(1)</script>',
+ "O'Brien",
+ "a & b",
+ "hive_process",
+ 'SELECT * FROM t WHERE x < 1 AND name = "foo"',
+ "hive_table<script>"
+ ];
+
+ payloads.forEach((payload) => {
+ expectSafeTooltipText(escapeHtml(payload));
+ expectSafeTooltipText(underscoreEscape(payload));
+ });
+ });
+});
diff --git a/dashboard/src/views/Lineage/atlas-lineage/src/Utils/index.js
b/dashboard/src/views/Lineage/atlas-lineage/src/Utils/index.js
index 4a146ef2f..21d9f61dc 100644
--- a/dashboard/src/views/Lineage/atlas-lineage/src/Utils/index.js
+++ b/dashboard/src/views/Lineage/atlas-lineage/src/Utils/index.js
@@ -25,10 +25,18 @@ const isEmpty = (value) =>
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0);
+const escapeHtml = (value) =>
+ String(value ?? "")
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+
String.prototype.trunc =
String.prototype.trunc ||
function(n) {
return this.length > n ? this.substr(0, n - 1) + "..." : this;
};
-export { LineageUtils, DataUtils, isEmpty };
\ No newline at end of file
+export { LineageUtils, DataUtils, isEmpty, escapeHtml };
\ No newline at end of file
diff --git a/dashboard/src/views/Lineage/atlas-lineage/src/index.js
b/dashboard/src/views/Lineage/atlas-lineage/src/index.js
index 8703401ee..8bd272c5c 100644
--- a/dashboard/src/views/Lineage/atlas-lineage/src/index.js
+++ b/dashboard/src/views/Lineage/atlas-lineage/src/index.js
@@ -19,7 +19,7 @@
import dagreD3 from "dagre-d3";
import { select, selection, event } from "d3-selection";
import { curveBasis } from "d3-shape";
-import { LineageUtils, DataUtils, isEmpty } from "./Utils";
+import { LineageUtils, DataUtils, isEmpty, escapeHtml } from "./Utils";
import d3Tip from "d3-tip";
import "./styles/style.scss";
@@ -543,7 +543,9 @@ export default class LineageHelper {
var htmlStr = "";
if (toolTipTitle) {
htmlStr =
- "<h5 style='text-align: center;'>" + toolTipTitle + "</h5>";
+ "<h5 style='text-align: center;'>" +
+ escapeHtml(toolTipTitle) +
+ "</h5>";
} else if (value.id !== this.guid) {
htmlStr =
"<h5 style='text-align: center;'>" +
@@ -553,18 +555,18 @@ export default class LineageHelper {
htmlStr +=
"<h5 class='text-center'><span style='color:#359f89'>" +
- value.toolTipLabel +
+ escapeHtml(value.toolTipLabel) +
"</span></h5> ";
if (value.typeName) {
htmlStr +=
"<h5 class='text-center'><span>(" +
- value.typeName +
+ escapeHtml(value.typeName) +
")</span></h5> ";
}
if (value.queryText) {
htmlStr +=
"<h5>Query: <span style='color:#359f89'>" +
- value.queryText +
+ escapeHtml(value.queryText) +
"</span></h5> ";
}
return "<div class='tip-inner-scroll'>" + htmlStr + "</div>";
diff --git a/dashboardv2/public/js/external_lib/atlas-lineage/dist/index.js
b/dashboardv2/public/js/external_lib/atlas-lineage/dist/index.js
index 40dc81b58..861ff4a67 100644
--- a/dashboardv2/public/js/external_lib/atlas-lineage/dist/index.js
+++ b/dashboardv2/public/js/external_lib/atlas-lineage/dist/index.js
@@ -1 +1 @@
-!function(t,e){"object"==typeof exports&&"object"==typeof
module?module.exports=e(require("platform"),require("dagreD3")):"function"==typeof
define&&define.amd?define(["platform","dagreD3"],e):"object"==typeof
exports?exports.LineageHelper=e(require("platform"),require("dagreD3")):t.LineageHelper=e(t.platform,t.dagreD3)}(window,(function(t,e){return
function(t){var e={};function n(r){if(e[r])return e[r].exports;var
i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i. [...]
\ No newline at end of file
+!function(t,e){"object"==typeof exports&&"object"==typeof
module?module.exports=e(require("underscore"),require("platform"),require("dagreD3")):"function"==typeof
define&&define.amd?define(["underscore","platform","dagreD3"],e):"object"==typeof
exports?exports.LineageHelper=e(require("underscore"),require("platform"),require("dagreD3")):t.LineageHelper=e(t.underscore,t.platform,t.dagreD3)}(window,(function(t,e,n){return
function(t){var e={};function n(r){if(e[r])return e[r].exports;var i [...]
\ No newline at end of file
diff --git a/dashboardv2/public/js/external_lib/atlas-lineage/src/index.js
b/dashboardv2/public/js/external_lib/atlas-lineage/src/index.js
index 9621bbd1d..50249004b 100644
--- a/dashboardv2/public/js/external_lib/atlas-lineage/src/index.js
+++ b/dashboardv2/public/js/external_lib/atlas-lineage/src/index.js
@@ -21,6 +21,7 @@ import { select, selection, event } from "d3-selection";
import { curveBasis } from "d3-shape";
import { LineageUtils, DataUtils } from "./Utils";
import d3Tip from "d3-tip";
+import _ from "underscore";
import "./styles/style.scss";
@@ -507,17 +508,17 @@ export default class LineageHelper {
var value = g.node(d);
var htmlStr = "";
if (toolTipTitle) {
- htmlStr = "<h5 style='text-align: center;'>" +
toolTipTitle + "</h5>";
+ htmlStr = "<h5 style='text-align: center;'>" +
_.escape(toolTipTitle) + "</h5>";
} else if (value.id !== this.guid) {
htmlStr = "<h5 style='text-align: center;'>" +
(value.isLineage ? "Lineage" : "Impact") + "</h5>";
}
- htmlStr += "<h5 class='text-center'><span
style='color:#359f89'>" + value.toolTipLabel + "</span></h5> ";
+ htmlStr += "<h5 class='text-center'><span
style='color:#359f89'>" + _.escape(value.toolTipLabel) + "</span></h5> ";
if (value.typeName) {
- htmlStr += "<h5 class='text-center'><span>(" +
value.typeName + ")</span></h5> ";
+ htmlStr += "<h5 class='text-center'><span>(" +
_.escape(value.typeName) + ")</span></h5> ";
}
if (value.queryText) {
- htmlStr += "<h5>Query: <span style='color:#359f89'>" +
value.queryText + "</span></h5> ";
+ htmlStr += "<h5>Query: <span style='color:#359f89'>" +
_.escape(value.queryText) + "</span></h5> ";
}
return "<div class='tip-inner-scroll'>" + htmlStr +
"</div>";
}
diff --git a/dashboardv2/public/js/external_lib/atlas-lineage/webpack.config.js
b/dashboardv2/public/js/external_lib/atlas-lineage/webpack.config.js
index 9457b386d..e3797b328 100644
--- a/dashboardv2/public/js/external_lib/atlas-lineage/webpack.config.js
+++ b/dashboardv2/public/js/external_lib/atlas-lineage/webpack.config.js
@@ -40,7 +40,8 @@ const config = {
"dagre-d3": "dagreD3",
platform: "platform",
dagre: "dagre",
- graphlib: "graphlib"
+ graphlib: "graphlib",
+ underscore: "underscore"
},
module: {
rules: [
diff --git a/dashboardv2/public/js/main.js b/dashboardv2/public/js/main.js
index 2e9da95d5..17fb5d59f 100644
--- a/dashboardv2/public/js/main.js
+++ b/dashboardv2/public/js/main.js
@@ -96,7 +96,7 @@ require.config({
'exports': ['d3-tip']
},
'LineageHelper': {
- 'deps': ['d3'],
+ 'deps': ['d3', 'underscore'],
},
'dagreD3': {
'deps': ['d3'],
diff --git a/dashboardv2/public/js/migration.js
b/dashboardv2/public/js/migration.js
index 451113105..8ad62561b 100644
--- a/dashboardv2/public/js/migration.js
+++ b/dashboardv2/public/js/migration.js
@@ -95,7 +95,7 @@ require.config({
'exports': ['d3-tip']
},
'LineageHelper': {
- 'deps': ['d3'],
+ 'deps': ['d3', 'underscore'],
},
'dagreD3': {
'deps': ['d3'],
diff --git a/dashboardv2/public/js/views/graph/ProfileBarChart.js
b/dashboardv2/public/js/views/graph/ProfileBarChart.js
index 78d2c904b..96c04888f 100644
--- a/dashboardv2/public/js/views/graph/ProfileBarChart.js
+++ b/dashboardv2/public/js/views/graph/ProfileBarChart.js
@@ -16,8 +16,21 @@
* limitations under the License.
*/
-define(["require", "d3", "d3-tip"], function(require, d3, d3Tip) {
+define(["require", "d3", "d3-tip", "underscore"], function(require, d3, d3Tip,
_) {
"use strict";
+
+ var buildTooltipHtml = function(d, type) {
+ return (
+ "<table><thead><tr><td colspan=\"3\"><strong class=\"x-value\">" +
+ _.escape(d.value) +
+ "</strong></td></tr></thead><tbody><tr><td class=\"key\">" +
+ _.escape(type) +
+ "</td><td class=\"value\">" +
+ _.escape(d.count) +
+ "</td></tr></tbody></table>"
+ );
+ };
+
var ProfileBarChart = {
render: function(options) {
var el = options.el,
@@ -103,7 +116,7 @@ define(["require", "d3", "d3-tip"], function(require, d3,
d3Tip) {
.attr("class", "d3-tip")
.offset([10, 0])
.html(function(d) {
- return '<table><thead><tr><td colspan="3"><strong
class="x-value">' + d.value + '</strong></td></tr></thead><tbody><tr><td
class="key">' + type + '</td><td class="value">' + d.count +
'</td></tr></tbody></table>';
+ return buildTooltipHtml(d, type);
});
// append the rectangles for the bar chart