aminghadersohi commented on code in PR #45575:
URL: https://github.com/apache/superset/pull/45575#discussion_r4350925836


##########
superset/mcp_service/utils/sanitization.py:
##########
@@ -70,11 +70,29 @@ def _strip_html_tags(value: str) -> str:
     # url_schemes=set() blocks all URL schemes in any remaining attributes
     cleaned = nh3.clean(decoded, tags=set(), url_schemes=set())
 
-    # Only restore & → & to preserve ampersands in display text (e.g. "A & 
B").
-    # Do NOT use html.unescape() here: nh3 may pass through HTML entities from
-    # the input (e.g. <script>), and a full unescape would re-introduce
-    # raw angle brackets, creating an XSS vector.
-    return cleaned.replace("&", "&")
+    # HTML tag-open states require an ASCII letter, slash, bang or question
+    # mark. Preserve literal comparisons like <30 and <$1M, but never decode
+    # a tag opener reconstructed by stripping intervening markup.
+    # Normalize removable characters before deciding whether a bracket is safe;
+    # otherwise a later Unicode strip could turn <\u200bscript into markup.
+    cleaned = _remove_dangerous_unicode(cleaned)
+    return re.sub(r"&lt;(?![A-Za-z/!?])", "<", cleaned).replace("&amp;", "&")

Review Comment:
   Fixed in 6143776c9a761201f4633bcf804243f5901094ce. I restore `>` and NBSP 
after tag stripping, preserving literal text and length caps without 
entity-semicolon filter rejections. The allowlist can intentionally leave other 
brackets escaped, so I also enforce the sanitized storage limit for 
generate/duplicate dashboard paths without Field caps.
   
   Regressions: 
`tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py::test_title_length_counts_plain_text`,
 
`tests/unit_tests/mcp_service/utils/test_sanitization.py::test_filter_value_preserves_greater_than_and_nbsp`,
 and 
`tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py::test_sibling_fields_preserve_literal_comparison`
 fail with the reviewed sanitizer and pass with this change. The reviewed-head 
sanitizer run produced 55 failed / 776 passed. The four 
`test_*_literal_less_than_storage_round_trip` tool tests gained `>`/NBSP cases: 
8 failed / 12 passed against reviewed source, all pass after. The seven touched 
Python files pass in Docker: 1,232 passed (fastmcp 3.4.7 / nh3 0.3.7 / pydantic 
2.14.1; manual/llm_compliance deselected).
   
   Siblings checked:
   - superset/mcp_service/chart/schemas.py:6369 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (generate/save=false/update chart 
names).
   - superset/mcp_service/dashboard/schemas.py:884 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (generate titles).
   - superset/mcp_service/dashboard/schemas.py:1050 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (update titles).
   - superset/mcp_service/dashboard/schemas.py:1531 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (duplicate titles).
   - superset/mcp_service/dashboard/schemas.py:1393 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (certified_by).
   - superset/mcp_service/dashboard/schemas.py:1414 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (certification_details).
   - superset/mcp_service/chart/schemas.py:879 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (temporal columns).
   - superset/mcp_service/chart/schemas.py:1003 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (column names).
   - superset/mcp_service/chart/schemas.py:1011 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (column labels).
   - superset/mcp_service/chart/schemas.py:1132 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (filter columns).
   - superset/mcp_service/chart/schemas.py:1143 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (scalar/list filter values).
   - superset/mcp_service/chart/schemas.py:1211 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Gantt columns).
   - superset/mcp_service/chart/schemas.py:2761 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Sunburst ranges).
   - superset/mcp_service/chart/schemas.py:3155 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (pivot comparison periods).
   - superset/mcp_service/chart/schemas.py:4628 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Bullet range/marker/marker-line 
labels).
   - superset/mcp_service/chart/schemas.py:4637 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Bullet ranges).
   - superset/mcp_service/chart/tool/generate_chart.py:255 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression; schema 
revalidation carries the sanitized title).
   - superset/mcp_service/chart/tool/update_chart.py — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression).
   - superset/mcp_service/dashboard/tool/generate_dashboard.py — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression).
   - superset/mcp_service/dashboard/tool/update_dashboard.py — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression).
   - superset/mcp_service/chart/schemas.py:6435 — not affected: Explore links 
and update_chart_preview have no title field; their config uses the shared 
field validators above.
   - superset/mcp_service/dataset/schemas.py — not affected: object 
names/descriptions do not use this HTML sanitizer; literal-text controls remain 
covered.
   - superset/mcp_service/theme/schemas.py — not affected: blank/length 
validation without HTML stripping.
   - superset/mcp_service/sql_lab/schemas.py — not affected: label/tab trimming 
without HTML stripping.
   - superset/mcp_service/utils/sanitization.py:sanitize_sql_expression — not 
affected: separate SQL canonicalization, with existing comparison-operator 
tests.
   - superset/mcp_service/utils/error_builder.py:54 — not affected: differently 
named diagnostic scrubber, not a caller of the shared sanitizer.
   - superset/charts/schemas.py — not affected: REST does not call the MCP 
sanitizer; the tooltip rendering changes also cover raw text originating in 
REST/Explore.
   



##########
tests/unit_tests/mcp_service/utils/test_sanitization.py:
##########
@@ -903,3 +904,95 @@ def 
test_data_label_followed_by_control_whitespace_is_preserved(
     """Whitespace after a comma-free data: label may be a tab or line break."""
     value = f"Substitute data:{separator}Jira support tickets"
     assert sanitize(value) == value.replace("\r\n", "\n")
+
+
[email protected](
+    "value",
+    [
+        "Users aged <30",
+        "Revenue <$1M",
+        "Love <3 charts",
+        "5 < 10",
+        "A < B",
+        "A <",
+        "A <> B",
+    ],
+)
+def test_strip_html_tags_preserves_non_tag_less_than(value: str) -> None:
+    """Preserve display text without decoding tag-opening brackets."""
+    assert _strip_html_tags(value) == value.replace(">", "&gt;")

Review Comment:
   Fixed in 6143776c9a761201f4633bcf804243f5901094ce. 
`tests/unit_tests/mcp_service/utils/test_sanitization.py::test_strip_html_tags_preserves_non_tag_less_than`
 asserts exact equality, including `A <> B`, `Users aged >30`, and NBSP 
comparisons. Those assertions fail with the reviewed sanitizer and pass after 
restoring the safe characters. The full touched Python selection passes in 
Docker (1,232 passed); filter, schema boundary, warning, resubmission and 
persistence assertions cover the same behavior rather than preserving the old 
asymmetry.
   
   Siblings checked:
   - superset/mcp_service/chart/schemas.py:6369 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (generate/save=false/update chart 
names).
   - superset/mcp_service/dashboard/schemas.py:884 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (generate titles).
   - superset/mcp_service/dashboard/schemas.py:1050 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (update titles).
   - superset/mcp_service/dashboard/schemas.py:1531 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (duplicate titles).
   - superset/mcp_service/dashboard/schemas.py:1393 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (certified_by).
   - superset/mcp_service/dashboard/schemas.py:1414 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (certification_details).
   - superset/mcp_service/chart/schemas.py:879 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (temporal columns).
   - superset/mcp_service/chart/schemas.py:1003 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (column names).
   - superset/mcp_service/chart/schemas.py:1011 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (column labels).
   - superset/mcp_service/chart/schemas.py:1132 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (filter columns).
   - superset/mcp_service/chart/schemas.py:1143 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (scalar/list filter values).
   - superset/mcp_service/chart/schemas.py:1211 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Gantt columns).
   - superset/mcp_service/chart/schemas.py:2761 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Sunburst ranges).
   - superset/mcp_service/chart/schemas.py:3155 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (pivot comparison periods).
   - superset/mcp_service/chart/schemas.py:4628 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Bullet range/marker/marker-line 
labels).
   - superset/mcp_service/chart/schemas.py:4637 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (Bullet ranges).
   - superset/mcp_service/chart/tool/generate_chart.py:255 — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression; schema 
revalidation carries the sanitized title).
   - superset/mcp_service/chart/tool/update_chart.py — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression).
   - superset/mcp_service/dashboard/tool/generate_dashboard.py — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression).
   - superset/mcp_service/dashboard/tool/update_dashboard.py — fixed in 
6143776c9a761201f4633bcf804243f5901094ce (storage/readback regression).
   - superset/mcp_service/chart/schemas.py:6435 — not affected: Explore links 
and update_chart_preview have no title field; their config uses the shared 
field validators above.
   - superset/mcp_service/dataset/schemas.py — not affected: object 
names/descriptions do not use this HTML sanitizer; literal-text controls remain 
covered.
   - superset/mcp_service/theme/schemas.py — not affected: blank/length 
validation without HTML stripping.
   - superset/mcp_service/sql_lab/schemas.py — not affected: label/tab trimming 
without HTML stripping.
   - superset/mcp_service/utils/sanitization.py:sanitize_sql_expression — not 
affected: separate SQL canonicalization, with existing comparison-operator 
tests.
   - superset/mcp_service/utils/error_builder.py:54 — not affected: differently 
named diagnostic scrubber, not a caller of the shared sanitizer.
   - superset/charts/schemas.py — not affected: REST does not call the MCP 
sanitizer; the tooltip rendering changes also cover raw text originating in 
REST/Explore.
   



##########
docs/developer_docs/extensions/mcp.md:
##########
@@ -28,6 +28,17 @@ under the License.
 
 Model Context Protocol (MCP) integration allows extensions to register custom 
AI agent capabilities that integrate seamlessly with Superset's MCP service. 
Extensions can provide both **tools** (executable functions) and **prompts** 
(interactive guidance) that AI agents can discover and use.
 
+## Chart and dashboard titles
+
+MCP chart names and dashboard titles preserve literal comparisons such as
+`Users aged <30`, `Revenue <$1M`, and `Love <3 charts`. HTML tags are stripped;

Review Comment:
   Fixed in 6143776c9a761201f4633bcf804243f5901094ce, including the follow-up 
in this PR. I HTML-escape plain-text fragments using the existing ECharts 
helper instead of treating labels as rich HTML. I also found the same 
core-filter use in Rose's drilled pie and changed it.
   
   Regression tests: 
`superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts::escapes
 literal comparison text in every Bullet tooltip`, 
`superset-frontend/plugins/plugin-chart-echarts/test/Rose/transformProps.test.ts::escapes
 literal series text in Rose tooltips (rich=%s)`, and 
`superset-frontend/plugins/plugin-chart-echarts/test/Rose/EchartsRose.test.tsx::drilled
 Rose tooltips preserve literal series and period text`. The three suites 
produce 5 failed / 27 passed with reviewed source, then 32 passed after the 
change. They inspect rendered DOM text and ensure label markup stays inert 
while intended tooltip formatting survives. I used the checkout's core source 
through a Jest mapper with cached dependencies. The rendering fix covers all 
input entry points, including Explore.
   
   Siblings checked:
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts:229 
— fixed in 6143776c9a761201f4633bcf804243f5901094ce (group prefix, metric, 
containing-range label and formatted value).
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts:261 
— fixed in 6143776c9a761201f4633bcf804243f5901094ce (range threshold tooltip).
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts:278 
— fixed in 6143776c9a761201f4633bcf804243f5901094ce (marker tooltip).
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Rose/transformProps.ts:136 — 
fixed in 6143776c9a761201f4633bcf804243f5901094ce (rich series tooltip, period 
text and formatted value).
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Rose/transformProps.ts:183 — 
fixed in 6143776c9a761201f4633bcf804243f5901094ce (plain series tooltip).
   - superset-frontend/plugins/plugin-chart-echarts/src/Rose/EchartsRose.tsx:81 
— fixed in 6143776c9a761201f4633bcf804243f5901094ce (drill-down pie tooltip).
   - superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts:1043 — 
not affected: this similarly named sanitizeHtml already HTML-escapes with 
ECharts encodeHTML; reused for these fixes.
   - 
superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts:149
 — not affected: both tooltip paths import the escaping helper from 
utils/series, not the core HTML filter.
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts:390 
— not affected: source/target labels already use the escaping helper.
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts:75 — 
not affected: raw names already use the escaping helper before tooltipHtml.
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts:73 
— not affected: raw names already use the escaping helper before tooltipHtml.
   - superset-frontend/plugins/plugin-chart-echarts/src/Radar/utils.ts:67 — not 
affected: label/value fragments import the escaping helper.
   - superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts:124 — 
not affected: series labels import the escaping helper.
   - 
superset-frontend/plugins/plugin-chart-echarts/src/utils/eChartOptionsSchema.ts:71
 — not affected: sanitizes explicitly HTML-valued option strings, not the 
plain-text label fragments changed here.
   - superset-frontend/packages/superset-ui-core/src/utils/tooltip.ts:78 — not 
affected: intentionally accepts rich HTML; the Pie/Funnel/Graph callers above 
escape plain text before calling it.
   - 
superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts — 
not affected: canvas axis/legend/mark-line labels are not HTML sinks; the 
common transformer covers MCP, REST and Explore inputs.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to