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">×</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"; + } }