This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git


The following commit(s) were added to refs/heads/master by this push:
     new f32d32d49 TAP5-2811: fixing possible XSS in Confirm mixin JS
f32d32d49 is described below

commit f32d32d49d686523213f5a91a273bfb7e70ef9af
Author: Thiago H. de Paula Figueiredo <thi...@arsmachina.com.br>
AuthorDate: Tue Aug 12 19:39:07 2025 -0300

    TAP5-2811: fixing possible XSS in Confirm mixin JS
    
    Thanks Yannick Dylla (https://github.com/ydylla) for bringing this to
    our attention!
---
 .../main/typescript/src/t5/core/confirm-click.ts   | 17 +++--
 .../main/typescript/src/t5/core/html-sanitizer.ts  | 84 ++++++++++++++++++++++
 tapestry-core/src/test/app1/ConfirmDemo.tml        |  2 +-
 .../integration/app1/ConfirmMixinTests.groovy      |  3 +
 .../integration/app1/pages/ConfirmDemo.java        |  5 ++
 5 files changed, 101 insertions(+), 10 deletions(-)

diff --git a/tapestry-core/src/main/typescript/src/t5/core/confirm-click.ts 
b/tapestry-core/src/main/typescript/src/t5/core/confirm-click.ts
index 1a804f036..8e7a82df8 100644
--- a/tapestry-core/src/main/typescript/src/t5/core/confirm-click.ts
+++ b/tapestry-core/src/main/typescript/src/t5/core/confirm-click.ts
@@ -17,7 +17,8 @@
  */
 
 import $ from "jquery";
-import  "bootstrap/modal";
+import "bootstrap/modal";
+import sanitizeHtml from "t5/core/html-sanitizer";
 
 /**
  * Dialog options.
@@ -40,7 +41,7 @@ interface DialogOptions {
 // options.okLabel - default "OK"
 // options.cancelLabel - default "Cancel"
 // options.ok - callback function, required
-const runDialog = function(options: DialogOptions) {
+export const runDialog = function(options: DialogOptions) {
 
   let confirmed = false;
 
@@ -50,12 +51,12 @@ const runDialog = function(options: DialogOptions) {
     <div class="modal-content">
       <div class="modal-header">
         <a class="close" data-dismiss="modal">&times;</a>
-        <h3>${options.title || "Confirm"}</h3>
+        <h3>${sanitizeHtml(options.title || "Confirm")}</h3>
       </div>
-      <div class="modal-body">${options.message}</div>
+      <div class="modal-body">${sanitizeHtml(options.message)}</div>
       <div class="modal-footer">
-        <button class="btn ${options.okClass || "btn-warning"}" 
data-dismiss="modal">${options.okLabel || "OK"}</button>
-        <button class="btn btn-default" 
data-dismiss="modal">${options.cancelLabel || "Cancel"}</button>
+        <button class="btn ${sanitizeHtml(options.okClass || "btn-warning")}" 
data-dismiss="modal">${sanitizeHtml(options.okLabel || "OK")}</button>
+        <button class="btn btn-default" 
data-dismiss="modal">${sanitizeHtml(options.cancelLabel || "Cancel")}</button>
       </div>
     </div>
   </div>
@@ -123,6 +124,4 @@ $("body").on("click", 
"[data-confirm-message]:not(.disabled)", function(event){
   // of the
   window.location.href = target.attr("href");
   return false;
-});
-
-export default runDialog;
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/tapestry-core/src/main/typescript/src/t5/core/html-sanitizer.ts 
b/tapestry-core/src/main/typescript/src/t5/core/html-sanitizer.ts
new file mode 100644
index 000000000..cd6921341
--- /dev/null
+++ b/tapestry-core/src/main/typescript/src/t5/core/html-sanitizer.ts
@@ -0,0 +1,84 @@
+// Copyright 2025 The Apache Software Foundation
+//
+// Licensed 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.
+
+/** 
+ * ## t5/core/html-sanitizer
+ *
+ * Provides a function that sanitizes HTML.
+ * @packageDocumentation
+ */
+export default function(html: string) {
+
+  if (html === null || html === undefined) {
+    return "";
+  }
+
+  // No elements, no problems
+  if (html.indexOf("<") < 0) {
+    return html;
+  }
+
+  // Mostly copied from
+  // 
https://gomakethings.com/how-to-sanitize-html-strings-with-vanilla-js-to-reduce-your-risk-of-xss-attacks/
+
+  let parser = new DOMParser();
+  let document = parser.parseFromString(html, "text/html");
+
+  // Remove <script> and <iframe> elements
+
+  let scripts = document.querySelectorAll("script, iframe");
+  for (let script of scripts) {
+    script.remove();
+  }
+
+  let root = document.body;
+
+  clean(root);
+
+  return root.innerHTML;
+
+}
+
+// Recursively cleans the elements
+function clean(element: HTMLElement) {
+  removeDangerousAttributes(element);
+  for (let child of element.children) {
+    clean(child as HTMLElement);
+  }
+}
+
+// Remove on[something] attributes
+function removeDangerousAttributes(element: HTMLElement): void {
+
+  let attributes = element.attributes;
+  if (attributes != null) {
+    for (let {name, value} of attributes) {
+      if (isDangerousAttribute(name, value)) {
+        element.removeAttribute(name);
+      }
+    }
+  }
+
+}
+
+const DANGEROUS_ATTRIBUTE_NAMES = ['src', 'href', 'xlink:href'];
+
+function isDangerousAttribute(name: string, value: string) {
+  value = value.replace(/\s+/g, '').toLowerCase();
+  return name.toLocaleLowerCase().startsWith("on") ||
+    (DANGEROUS_ATTRIBUTE_NAMES.includes(name) && (
+      value.includes("javascript:") ||
+      value.includes("data:text/html")
+    ));
+}
diff --git a/tapestry-core/src/test/app1/ConfirmDemo.tml 
b/tapestry-core/src/test/app1/ConfirmDemo.tml
index 774306fea..b97aa4768 100644
--- a/tapestry-core/src/test/app1/ConfirmDemo.tml
+++ b/tapestry-core/src/test/app1/ConfirmDemo.tml
@@ -2,7 +2,7 @@
 
     <h1>Confirm Mixin Demo</h1>
 
-    <t:actionlink t:id="confirmed" class="btn btn-primary" 
t:mixins="confirm">Click This</t:actionlink>
+    <t:actionlink t:id="confirmed" class="btn btn-primary" t:mixins="confirm" 
t:title="prop:title">Click This</t:actionlink>
 
 </html>
 
diff --git 
a/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/ConfirmMixinTests.groovy
 
b/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/ConfirmMixinTests.groovy
index 99cae0e6d..87d5ce6ae 100644
--- 
a/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/ConfirmMixinTests.groovy
+++ 
b/tapestry-core/src/test/groovy/org/apache/tapestry5/integration/app1/ConfirmMixinTests.groovy
@@ -12,6 +12,9 @@ class ConfirmMixinTests extends App1TestCase {
         click "link=Click This"
 
         waitForVisible "css=.modal-dialog"
+        
+        // TAP5-2811: XSS
+        assertText("css=.modal-content h3", "something else");
 
         clickAndWait "css=.modal-dialog .btn-warning"
 
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ConfirmDemo.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ConfirmDemo.java
index 34270204f..c5b25001c 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ConfirmDemo.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ConfirmDemo.java
@@ -12,4 +12,9 @@ public class ConfirmDemo
     {
         alertManager.info("Action was confirmed.");
     }
+    
+    public String getTitle()
+    {
+        return "some<span><script>window.alert('ouch1');</script></span><em 
onclick=\"window.alert('ouch2')\">thing</em> else";
+    }
 }

Reply via email to