Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ZK-5716: Errorbox contains inline script #3199

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions zk/src/main/resources/web/js/zk/crashmsg.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -49,7 +49,7 @@ window.zkInitCrashTimer = setTimeout(function () {
else
zkErrorCode = 5;
}

if (!window.zkShowCrashMessage) {
window.zkShowCrashMessage = function () {
var styleHTML = '<style> a:visited {color: white;} </style>',
Expand All @@ -62,7 +62,7 @@ window.zkInitCrashTimer = setTimeout(function () {
copyrightHTML = '<div style="text-align: right; margin-top: -18px">\
<span style="font-size: 10px;">powered by </span>\
<span style="font-size: 14px;"><a href="http://www.zkoss.org/">ZK</a></span></div>',
btnHTML = '<button style="margin-top: 10px" onclick="location.reload();">Reload page</button>';
btnHTML = '<button id="z-crash-button" style="margin-top: 10px">Reload page</button>';
switch (zkErrorCode) {
case 1:
msgHTML = '<p>Error code 1: ZK error, before mounting. </p>' + msgHTML;
Expand All @@ -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);
Expand Down
42 changes: 34 additions & 8 deletions zk/src/main/resources/web/js/zk/zk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,29 @@ function doLog(): void {
if (_logmsg) {
var console = document.getElementById('zk_log') as HTMLTextAreaElement;
if (!console) {
jq(document.body).append(/*safe*/
'<div id="zk_logbox" class="z-log">'
+ '<button class="z-button" onclick="jq(\'#zk_logbox\').remove()">X</button><br/>'
+ '<textarea id="zk_log" rows="10"></textarea></div>');
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;
Expand Down Expand Up @@ -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 = '<div class="z-error" id="' + id + '">'
+ '<div id="' + id + '-p">'
+ '<div class="errornumbers">' + (++_errcnt) + ' Errors</div>'
+ '<div class="button"' + click + '="zk._Erbx.remove()">'
+ '<div id="' + id + '-remove-btn" class="button">'
+ '<i class="z-icon-times"></i></div>'
+ '<div class="button"' + click + '="zk._Erbx.redraw()">'
+ '<div id="' + id + '-refresh-btn" class="button">'
+ '<i class="z-icon-refresh"></i></div></div>'
+ '<div class="messagecontent"><div class="messages">'
+ zUtl.encodeXML(msg, {multiline: true}) + '</div></div></div>';

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 {
Expand Down Expand Up @@ -2179,7 +2205,7 @@ declare namespace _zk {

// ./drag
export let dragging: boolean;

// ./widget
export let timeout: number;
export let groupingDenied: boolean;
Expand Down
1 change: 1 addition & 0 deletions zkdoc/release-note
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="javascript:;">
"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)<script(\\s)*","<script nonce=\"" + hex + "\" ");

handleCompress((HttpServletRequest) request, response, replacedContent);
logger.log(Level.FINE, "filtered " + request + " \nwith nonce: " + hex);
}

protected void handleCompress(HttpServletRequest request, ServletResponse response, String replacedContent) throws IOException {
if(compress) {
// Do gzip after CSP rewriting
byte[] data = replacedContent.getBytes(response.getCharacterEncoding());
if (data.length > 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());
}

}
}
56 changes: 56 additions & 0 deletions zktest/src/main/webapp/test2/B101-ZK-5716-web.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">


<filter>
<filter-name>zkCspFilter</filter-name>
<filter-class>org.zkoss.zktest.util.ZkCspFilterStrictDynamic</filter-class>
<!-- optional init-param to choose the digest algorithm, default SHA-1 -->
<!-- <init-param> -->
<!-- <param-name>digest-algorithm</param-name> -->
<!-- <param-value>SHA-256</param-value> -->
<!-- </init-param> -->

<!-- optional init-param to write a different CSP header, use %s as placeholder for the nonce value. -->
<!-- <init-param> -->
<!-- <param-name>csp-header</param-name> -->
<!-- <param-value>script-src 'strict-dynamic' 'nonce-%s' 'unsafe-inline' 'unsafe-eval';object-src 'none';base-uri 'none';</param-value> -->
<!-- </init-param> -->

<!-- optional init-param to compress the resulting response after adding CSP content
<init-param>
<param-name>compress</param-name>
<param-value>false</param-value>
</init-param> -->
</filter>
<filter-mapping>
<filter-name>zkCspFilter</filter-name>
<url-pattern>*.zul</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>zkCspFilter</filter-name>
<url-pattern>*/</url-pattern>
</filter-mapping>

<!-- override zktest's web.xml to disable compression-->
<servlet>
<description>ZK loader for ZUML pages</description>
<servlet-name>zkLoader</servlet-name>
<servlet-class>org.zkoss.zktest.http.ZKTestServlet</servlet-class>
<init-param>
<param-name>update-uri</param-name>
<param-value>/zkau</param-value>
</init-param>
<init-param>
<param-name>resource-uri</param-name>
<param-value>/zkres</param-value>
</init-param>
<init-param>
<param-name>compress</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup><!-- Must -->
</servlet>
</web-app>
Loading
Loading