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> = {
+               "&": "&amp;",
+               "<": "&lt;",
+               ">": "&gt;",
+               '"': "&quot;",
+               "'": "&#x27;",
+               "`": "&#x60;"
+       };
+
+       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("&lt;img");
+                       expect(escaped).toContain("&quot;");
+                       expectSafeTooltipText(escaped);
+               });
+
+               it("escapes script tags", () => {
+                       const escaped = 
escapeHtml('<script>alert("x")</script>');
+                       
expect(escaped).toBe("&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;");
+                       expectSafeTooltipText(escaped);
+               });
+
+               it("escapes ampersand", () => {
+                       expect(escapeHtml("a & b")).toBe("a &amp; b");
+               });
+
+               it("double-encodes already-encoded ampersand entities safely", 
() => {
+                       expect(escapeHtml("a &amp; b")).toBe("a &amp;amp; b");
+                       expectSafeTooltipText(escapeHtml("a &amp; b"));
+               });
+
+               it("escapes single quotes", () => {
+                       expect(escapeHtml("O'Brien")).toBe("O&#39;Brien");
+               });
+
+               it("escapes double quotes", () => {
+                       expect(escapeHtml('say "hello"')).toBe("say 
&quot;hello&quot;");
+               });
+
+               it("escapes all special characters in one payload", () => {
+                       const payload = `<a href='x'>&"test"`;
+                       const escaped = escapeHtml(payload);
+                       expect(escaped).toBe("&lt;a 
href=&#39;x&#39;&gt;&amp;&quot;test&quot;");
+                       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&lt;script&gt;alert(1)&lt;/script&gt;");
+                       expectSafeTooltipText(escaped);
+               });
+
+               it("encodes typeName values", () => {
+                       const escaped = escapeHtml("hive_table<script>");
+                       expect(escaped).toBe("hive_table&lt;script&gt;");
+                       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("&lt;");
+                       expect(escaped).toContain("&gt;");
+                       expect(escaped).toContain("&quot;");
+                       expectSafeTooltipText(escaped);
+               });
+
+               it("encodes static toolTipTitle values", () => {
+                       expect(escapeHtml("Type")).toBe("Type");
+                       
expect(escapeHtml('Type<script>')).toBe("Type&lt;script&gt;");
+               });
+       });
+
+       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("&lt;");
+               expect(escaped).toContain("&quot;");
+               expectSafeTooltipText(escaped);
+       });
+
+       it("encodes lineage typeName values", () => {
+               const escaped = underscoreEscape("hive_table<script>");
+               expect(escaped).toBe("hive_table&lt;script&gt;");
+               expectSafeTooltipText(escaped);
+       });
+
+       it("encodes backtick characters used in entity names", () => {
+               expect(underscoreEscape("db`table")).toBe("db&#x60;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, "&amp;")
+               .replace(/</g, "&lt;")
+               .replace(/>/g, "&gt;")
+               .replace(/"/g, "&quot;")
+               .replace(/'/g, "&#39;");
+
 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

Reply via email to