From 3f3b9dedb9ffa5eb72678cd03447651ff557f6a6 Mon Sep 17 00:00:00 2001 From: Jumper Chen Date: Wed, 11 Sep 2024 14:44:30 +0800 Subject: [PATCH] ZK-5716: Errorbox contains inline script --- zk/src/main/resources/web/js/zk/crashmsg.ts | 11 +- zk/src/main/resources/web/js/zk/zk.ts | 42 +++- zkdoc/release-note | 1 + .../zktest/util/ZkCspFilterStrictDynamic.java | 210 ++++++++++++++++++ .../main/webapp/test2/B101-ZK-5716-web.xml | 56 +++++ zktest/src/main/webapp/test2/B101-ZK-5716.zul | 24 ++ .../src/main/webapp/test2/config.properties | 1 + .../zktest/zats/test2/B101_ZK_5716Test.java | 54 +++++ .../resources/web/js/zul/db/mold/calendar.js | 14 +- .../web/js/zul/menu/mold/menupopup.js | 10 +- .../resources/web/js/zul/mesh/HeaderWidget.ts | 4 +- .../resources/web/js/zul/sel/mold/listbox.js | 11 +- .../resources/web/js/zul/sel/mold/tree.js | 15 +- .../main/resources/web/js/zul/wnd/Window.ts | 3 +- 14 files changed, 412 insertions(+), 44 deletions(-) create mode 100644 zktest/src/main/java/org/zkoss/zktest/util/ZkCspFilterStrictDynamic.java create mode 100644 zktest/src/main/webapp/test2/B101-ZK-5716-web.xml create mode 100644 zktest/src/main/webapp/test2/B101-ZK-5716.zul create mode 100644 zktest/src/test/java/org/zkoss/zktest/zats/test2/B101_ZK_5716Test.java diff --git a/zk/src/main/resources/web/js/zk/crashmsg.ts b/zk/src/main/resources/web/js/zk/crashmsg.ts index 54d24d07a2c..e79819a8e33 100644 --- a/zk/src/main/resources/web/js/zk/crashmsg.ts +++ b/zk/src/main/resources/web/js/zk/crashmsg.ts @@ -1,7 +1,7 @@ /* crashmsg.ts Purpose: To remove "processing" mask and animation if the execution time exceeds the parameterized period - + Description: zkErrorCode has 5 value: * 1 -> before mounting zk @@ -12,7 +12,7 @@ Customization in zk.xml: * init crash page layout by defining window.zkShowCrashMessage function * init crash timeout by giving a number(sec) - + History: Wed, Nov 12, 2014 5:10:36 PM, Created by Chunfu @@ -49,7 +49,7 @@ window.zkInitCrashTimer = setTimeout(function () { else zkErrorCode = 5; } - + if (!window.zkShowCrashMessage) { window.zkShowCrashMessage = function () { var styleHTML = '', @@ -62,7 +62,7 @@ window.zkInitCrashTimer = setTimeout(function () { copyrightHTML = '
\ powered by \ ZK
', - btnHTML = ''; + btnHTML = ''; switch (zkErrorCode) { case 1: msgHTML = '

Error code 1: ZK error, before mounting.

' + msgHTML; @@ -84,6 +84,9 @@ window.zkInitCrashTimer = setTimeout(function () { body.style.background = 'rgb(35,48,64)'; // eslint-disable-next-line @microsoft/sdl/no-inner-html body.innerHTML = divHTML; + document.getElementById('z-crash-button')?.addEventListener('click', () => { + location.reload(); + }); }; } window.zkShowCrashMessage(zkErrorCode); diff --git a/zk/src/main/resources/web/js/zk/zk.ts b/zk/src/main/resources/web/js/zk/zk.ts index d79c4b5649e..cb665594cb2 100644 --- a/zk/src/main/resources/web/js/zk/zk.ts +++ b/zk/src/main/resources/web/js/zk/zk.ts @@ -295,10 +295,29 @@ function doLog(): void { if (_logmsg) { var console = document.getElementById('zk_log') as HTMLTextAreaElement; if (!console) { - jq(document.body).append(/*safe*/ -'
' -+ '
' -+ '
'); + const logBox = document.createElement('div'), + closeButton = document.createElement('button'), + lineBreak = document.createElement('br'), + textArea = document.createElement('textarea'); + + logBox.id = 'zk_logbox'; + logBox.className = 'z-log'; + + closeButton.className = 'z-button'; + closeButton.textContent = 'X'; + closeButton.addEventListener('click', () => { + logBox.remove(); + }); + + textArea.id = 'zk_log'; + textArea.rows = 10; + + logBox.appendChild(closeButton); + logBox.appendChild(lineBreak); + logBox.appendChild(textArea); + + document.body.appendChild(logBox); + console = document.getElementById('zk_log') as HTMLTextAreaElement; } console.value += _logmsg; @@ -2070,19 +2089,26 @@ _zk._Erbx = class _Erbx extends ZKObject { //used in HTML tags super(); var id = 'zk_err', $id = '#' + id, - click = _zk.mobile ? ' ontouchstart' : ' onclick', + click = _zk.mobile ? 'touchstart' : 'click', // Use zUtl.encodeXML -- Bug 1463668: security html = '
' + '
' + '
' + (++_errcnt) + ' Errors
' - + '
' + + '
' + '
' - + '
' + + '
' + '
' + '
' + zUtl.encodeXML(msg, {multiline: true}) + '
'; jq(document.body).append(/*safe*/ html); + document.getElementById(id + '-remove-btn')?.addEventListener(click, () => { + _Erbx.remove(); + }); + document.getElementById(id + '-refresh-btn')?.addEventListener(click, () => { + _Erbx.redraw(); + }); + _erbx = this; this.id = id; try { @@ -2179,7 +2205,7 @@ declare namespace _zk { // ./drag export let dragging: boolean; - + // ./widget export let timeout: number; export let groupingDenied: boolean; diff --git a/zkdoc/release-note b/zkdoc/release-note index eb6d3b07b26..6fa0c1ad43c 100644 --- a/zkdoc/release-note +++ b/zkdoc/release-note @@ -13,6 +13,7 @@ ZK 10.1.0 ZK-5677: Executions.schedule cause infinite loop if async event causes exception ZK-5696: Nested Shadow element fails in ZK 10 ZK-5703: Debug messages shouldn't be created if debug is disabled, may cause side effects + ZK-5716: Errorbox contains inline script * Upgrade Notes + Remove Htmls.encodeJavaScript(), Strings.encodeJavaScript(), Strings.escape() with Strings.ESCAPE_JAVASCRIPT, and replace them with OWASP Java Encoder APIs instead. diff --git a/zktest/src/main/java/org/zkoss/zktest/util/ZkCspFilterStrictDynamic.java b/zktest/src/main/java/org/zkoss/zktest/util/ZkCspFilterStrictDynamic.java new file mode 100644 index 00000000000..9700dbc11c8 --- /dev/null +++ b/zktest/src/main/java/org/zkoss/zktest/util/ZkCspFilterStrictDynamic.java @@ -0,0 +1,210 @@ +/* ZkCspFilterStrictDynamic.java + + Purpose: + + Description: + + History: + 5:03 PM 2023/7/31, Created by jumperchen + +Copyright (C) 2023 Potix Corporation. All Rights Reserved. +*/ +package org.zkoss.zktest.util; + +/** + * @author jumperchen + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.zkoss.web.servlet.http.Https; +import org.zkoss.zk.ui.sys.DigestUtilsHelper; + +/** + * using strict-dynamic then 'unsafe-inline' will be ignored. + * @author jumperchen, Hawk Chen + */ +public class ZkCspFilterStrictDynamic implements Filter { + + public static final String DEFAULT_CSP = "script-src " + + " 'unsafe-eval' " + + " 'strict-dynamic' 'nonce-%s' " + //https://content-security-policy.com/strict-dynamic/ + " 'unsafe-hashes' " + //https://content-security-policy.com/unsafe-hashes/ + " 'sha256-lfXlPY3+MCPOPb4mrw1Y961+745U3WlDQVcOXdchSQc=';" + // allow + "object-src 'none';base-uri 'none';"; + private Logger logger = Logger.getLogger(ZkCspFilterStrictDynamic.class.getName()); + + private static final SecureRandom RNG = new SecureRandom(); + private String cspHeader; + private boolean compress; + private MessageDigest _digest; + private String hex; + + public void init(FilterConfig filterConfig) throws ServletException { + // we can pass init parameters from web.xml here by the filterConfig object. + logger.log(Level.INFO, "Initialized CSP filter"); + cspHeader = (filterConfig.getInitParameter("csp-header") == null || filterConfig.getInitParameter("csp-header").isEmpty()) ? DEFAULT_CSP : filterConfig.getInitParameter("csp-header"); + _digest = DigestUtilsHelper.getDigest((filterConfig.getInitParameter("digest-algorithm") == null || filterConfig.getInitParameter("digest-algorithm").isEmpty()) ? "SHA-1" : filterConfig.getInitParameter("digest-algorithm")); + compress = !"false".equals(filterConfig.getInitParameter("compress")); + hex = bytesToHex(_digest.digest(Integer.toString(RNG.nextInt()).getBytes())); + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(2 * bytes.length); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletResponse servletResponse = (HttpServletResponse) response; + servletResponse.setHeader("Content-Security-Policy", String.format(cspHeader, hex)); + + CapturingResponseWrapper capturingResponseWrapper = new CapturingResponseWrapper( + servletResponse); + chain.doFilter(request, capturingResponseWrapper); + + String content = capturingResponseWrapper.getCaptureAsString(); + String replacedContent = content.replaceAll("(?i) 200) { + byte[] bs = Https.gzip(request, (HttpServletResponse) response, null, data); + if (bs != null) + data = bs; + } + + response.setContentLength(data.length); + response.getOutputStream().write(data); + response.flushBuffer(); + }else { + response.getWriter().write(replacedContent); + } + } + + public void destroy() { + } + + private static class CapturingResponseWrapper + extends HttpServletResponseWrapper { + private final ByteArrayOutputStream capture; + private ServletOutputStream output; + private PrintWriter writer; + + public CapturingResponseWrapper(HttpServletResponse response) { + super(response); + capture = new ByteArrayOutputStream(response.getBufferSize()); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (writer != null) { + throw new IllegalStateException( + "getWriter() has already been called on this response."); + } + + final ServletOutputStream outputStream = super.getOutputStream(); + if (output == null) { + output = new ServletOutputStream() { + public boolean isReady() { + return outputStream.isReady(); + } + + public void setWriteListener(WriteListener writeListener) { + outputStream.setWriteListener(writeListener); + } + + @Override + public void write(int b) throws IOException { + capture.write(b); + } + + @Override + public void flush() throws IOException { + capture.flush(); + } + + @Override + public void close() throws IOException { + capture.close(); + } + }; + } + + return output; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (output != null) { + throw new IllegalStateException( + "getOutputStream() has already been called on this response."); + } + + if (writer == null) { + writer = new PrintWriter(new OutputStreamWriter(capture, + getCharacterEncoding())); + } + + return writer; + } + + @Override + public void flushBuffer() throws IOException { + super.flushBuffer(); + + if (writer != null) { + writer.flush(); + } else if (output != null) { + output.flush(); + } + } + + public byte[] getCaptureAsBytes() throws IOException { + if (writer != null) { + writer.close(); + } else if (output != null) { + output.close(); + } + + return capture.toByteArray(); + } + + public String getCaptureAsString() throws IOException { + return new String(getCaptureAsBytes(), getCharacterEncoding()); + } + + } +} \ No newline at end of file diff --git a/zktest/src/main/webapp/test2/B101-ZK-5716-web.xml b/zktest/src/main/webapp/test2/B101-ZK-5716-web.xml new file mode 100644 index 00000000000..59dbc7eccb4 --- /dev/null +++ b/zktest/src/main/webapp/test2/B101-ZK-5716-web.xml @@ -0,0 +1,56 @@ + + + + + + zkCspFilter + org.zkoss.zktest.util.ZkCspFilterStrictDynamic + + + + + + + + + + + + + + + + zkCspFilter + *.zul + + + zkCspFilter + */ + + + + + ZK loader for ZUML pages + zkLoader + org.zkoss.zktest.http.ZKTestServlet + + update-uri + /zkau + + + resource-uri + /zkres + + + compress + false + + 1 + + \ No newline at end of file diff --git a/zktest/src/main/webapp/test2/B101-ZK-5716.zul b/zktest/src/main/webapp/test2/B101-ZK-5716.zul new file mode 100644 index 00000000000..4ed14ef83f5 --- /dev/null +++ b/zktest/src/main/webapp/test2/B101-ZK-5716.zul @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/zktest/src/main/webapp/test2/config.properties b/zktest/src/main/webapp/test2/config.properties index cd86dca7f0e..3c7197f4a58 100644 --- a/zktest/src/main/webapp/test2/config.properties +++ b/zktest/src/main/webapp/test2/config.properties @@ -3132,6 +3132,7 @@ B90-ZK-4431.zul=A,E,Multislider ##zats##B101-ZK-5659-2.zul=A,E,ClientMVVM,Tree,Model,ROD ##zats##B101-ZK-5677.zul=A,E,Scheduler,Event,Exception ##zats##B101-ZK-5696.zul=A,E,NestedShadow,ServerMVVM,ForEach,Differ +##zats##B101-ZK-5716.zul=A,E,CSP,Security,Content-Security-Policy,Unsafe-inline ## # Features - 3.0.x version diff --git a/zktest/src/test/java/org/zkoss/zktest/zats/test2/B101_ZK_5716Test.java b/zktest/src/test/java/org/zkoss/zktest/zats/test2/B101_ZK_5716Test.java new file mode 100644 index 00000000000..7acf612832d --- /dev/null +++ b/zktest/src/test/java/org/zkoss/zktest/zats/test2/B101_ZK_5716Test.java @@ -0,0 +1,54 @@ +/* B101_ZK_5716Test.java + + Purpose: + + Description: + + History: + 4:17 PM 2024/9/10, Created by jumperchen + +Copyright (C) 2024 Potix Corporation. All Rights Reserved. +*/ +package org.zkoss.zktest.zats.test2; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.openqa.selenium.support.ui.WebDriverWait; + +import org.zkoss.test.webdriver.ExternalWebXml; +import org.zkoss.test.webdriver.ForkJVMTestOnly; +import org.zkoss.test.webdriver.WebDriverTestCase; + +/** + * @author jumperchen + */ +@ForkJVMTestOnly +public class B101_ZK_5716Test extends WebDriverTestCase { + @RegisterExtension + public static final ExternalWebXml CONFIG = new ExternalWebXml(B101_ZK_5716Test.class); + + @Test + public void test() { + connect(); + + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); // 10 seconds timeout + + click(jq(".z-error .button:eq(1)")); + assertNoJSError(); + wait.until(d -> jq(".z-error").isVisible()); + + click(jq(".z-error .button:eq(0)")); + waitResponse(); + assertNoJSError(); + assertFalse(jq(".z-error").isVisible()); + + click(jq(".z-log .z-button")); + waitResponse(); + assertNoJSError(); + assertFalse(jq(".z-log").isVisible()); + } +} diff --git a/zul/src/main/resources/web/js/zul/db/mold/calendar.js b/zul/src/main/resources/web/js/zul/db/mold/calendar.js index eb3a241ccde..a8e3970f0bf 100644 --- a/zul/src/main/resources/web/js/zul/db/mold/calendar.js +++ b/zul/src/main/resources/web/js/zul/db/mold/calendar.js @@ -18,17 +18,15 @@ function calendar$mold$(out) { var renderer = zul.db.Renderer, uuid = this.uuid, view = this._view, - tagnm = 'button', localizedSymbols = this.getLocalizedSymbols(), icon = this.$s('icon'), outRangeL = this.isOutOfRange(true) ? ' disabled="disabled" aria-disabled="true"' : '', outRangeR = this.isOutOfRange() ? ' disabled="disabled" aria-disabled="true"' : '', showTodayLink = this._showTodayLink; - + // header - out.push('
<', tagnm, ' id="', uuid, - '-a" tabindex="-1" onclick="return false;" href="javascript:;" class="z-focus-a">
'); - + switch (view) { case 'day': renderer.dayView(this, out, localizedSymbols); @@ -52,13 +50,13 @@ function calendar$mold$(out) { renderer.decadeView(this, out, localizedSymbols); break; } - + if (showTodayLink) { out.push(''); } - + out.push('
'); } diff --git a/zul/src/main/resources/web/js/zul/menu/mold/menupopup.js b/zul/src/main/resources/web/js/zul/menu/mold/menupopup.js index ef141921cc0..45740a0390c 100644 --- a/zul/src/main/resources/web/js/zul/menu/mold/menupopup.js +++ b/zul/src/main/resources/web/js/zul/menu/mold/menupopup.js @@ -13,12 +13,10 @@ This program is distributed under LGPL Version 2.1 in the hope that it will be useful, but WITHOUT ANY WARRANTY. */ function menupopup$mold$(out) { - var uuid = this.uuid, - tags = 'button'; - out.push('<', tags, ' id="', uuid, - '-a" tabindex="-1" onclick="return false;" href="javascript:;"', - ' class="z-focus-a" aria-hidden="true">
    '); + var uuid = this.uuid; + out.push('
      '); for (var w = this.firstChild; w; w = w.nextSibling) w.redraw(out); diff --git a/zul/src/main/resources/web/js/zul/mesh/HeaderWidget.ts b/zul/src/main/resources/web/js/zul/mesh/HeaderWidget.ts index c87c3d8cbf1..a2e3361e32b 100644 --- a/zul/src/main/resources/web/js/zul/mesh/HeaderWidget.ts +++ b/zul/src/main/resources/web/js/zul/mesh/HeaderWidget.ts @@ -739,8 +739,8 @@ export abstract class HeaderWidget extends zul.LabelImageWidget
'); if (this._nativebar && this.frozen) { out.push('
'); diff --git a/zul/src/main/resources/web/js/zul/sel/mold/tree.js b/zul/src/main/resources/web/js/zul/sel/mold/tree.js index 914500c540a..e9857bf7955 100644 --- a/zul/src/main/resources/web/js/zul/sel/mold/tree.js +++ b/zul/src/main/resources/web/js/zul/sel/mold/tree.js @@ -1,9 +1,9 @@ /* tree.js Purpose: - + Description: - + History: Wed Jun 10 15:30:46 2009, Created by jumperchen @@ -17,8 +17,7 @@ function tree$mold$(out) { innerWidth = zUtl.encodeXML(this.getInnerWidth()), width = innerWidth === '100%' ? ' width="100%"' : '', wdStyle = innerWidth !== '100%' ? 'width:' + innerWidth : '', - inPaging = this.inPagingMold(), pgpos, - tag = 'button'; + inPaging = this.inPagingMold(), pgpos; out.push('');//tabindex attribute will be set in the button //top paging @@ -77,12 +76,12 @@ function tree$mold$(out) { if (this.domPad_ && !inPaging) this.domPad_(out, '-bpad'); - out.push('<', tag, ' style="top:', jq.px(this._anchorTop), ';left:', jq.px(this._anchorLeft), '" id="', uuid, - '-a" onclick="return false;" href="javascript:;" class="z-focus-a"'); - var tabindex = this._tabindex; // Feature ZK-2531 + out.push('
', '
'); + out.push('>
'); if (this._nativebar && this.frozen) { out.push('
'); diff --git a/zul/src/main/resources/web/js/zul/wnd/Window.ts b/zul/src/main/resources/web/js/zul/wnd/Window.ts index c6e7c6334f3..0342f4e0ab2 100644 --- a/zul/src/main/resources/web/js/zul/wnd/Window.ts +++ b/zul/src/main/resources/web/js/zul/wnd/Window.ts @@ -175,8 +175,7 @@ function _doModal(wgt: zul.wnd.Window): void { zIndex: wgt._zIndex as number, visible: realVisible }); - var tag = 'button'; - jq('#' + wgt.uuid + '-mask').append('<' + tag + ' id="' + wgt.uuid + '-mask-a" style="top:0;left:0" onclick="return false;" href="javascript:;" class="z-focus-a" aria-hidden="true" tabindex="-1">'); + jq('#' + wgt.uuid + '-mask').append(/*safe*/ ''); wgt._anchor = jq('#' + wgt.uuid + '-mask-a')[0]; } if (realVisible)