wgt.setAttr("value2")
will be invoked at the client
* accordingly.
*
+ * Note: the String type value will be escaped by {@link Strings#escapeJavaScript} and {@link XMLs#escapeXML}.
+ * To allow the safe HTML, use {@link org.zkoss.zk.ui.SafeHtmlValue} to wrap the value. (since 10.0.0)
* @param append whether to append the updates of properties with the same
* name. If false, only the last value of the same property will be sent
* to the client.
@@ -1860,8 +1863,13 @@ protected void smartUpdate(String attr, Object value) {
* @see #smartUpdate(String, Object)
*/
protected void smartUpdate(String attr, Object value, boolean append) {
- if (_page != null)
+ if (_page != null) {
+ if (value instanceof String) {
+ // no need to use Strings.escapeJavaScript() here.
+ value = XMLs.escapeXML((String) value);
+ }
getAttachedUiEngine().addSmartUpdate(this, attr, value, append);
+ }
}
/** A special smart update to update a value in int.
diff --git a/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java b/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java
index ba5144661a2..ca3156b795f 100644
--- a/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java
+++ b/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java
@@ -294,8 +294,8 @@ private static boolean startsWith(StringBuffer sb, String tag, int start) {
protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
super.renderProperties(renderer);
- render(renderer, "prolog", getPrologHalf());
- render(renderer, "epilog", getEpilogHalf());
+ render(renderer, "prolog", SafeHtmlValue.valueOf(getPrologHalf()));
+ render(renderer, "epilog", SafeHtmlValue.valueOf(getEpilogHalf()));
}
private String getPrologHalf() {
diff --git a/zk/src/main/java/org/zkoss/zk/ui/SafeHtmlValue.java b/zk/src/main/java/org/zkoss/zk/ui/SafeHtmlValue.java
new file mode 100644
index 00000000000..c0a4a3077a2
--- /dev/null
+++ b/zk/src/main/java/org/zkoss/zk/ui/SafeHtmlValue.java
@@ -0,0 +1,78 @@
+/* SafeHtmlValue.java
+
+ Purpose:
+
+ Description:
+
+ History:
+ 2:39 PM 2023/12/20, Created by jumperchen
+
+Copyright (C) 2023 Potix Corporation. All Rights Reserved.
+*/
+package org.zkoss.zk.ui;
+
+import java.util.Objects;
+
+import org.zkoss.json.JSONValue;
+
+/**
+ * A string-like value that is safe to be used as HTML content.
+ * @author jumperchen
+ * @since 10.0.0
+ */
+public class SafeHtmlValue implements org.zkoss.json.JSONAware, java.io.Serializable {
+ private static final long serialVersionUID = 202312201440L;
+ private final String _value;
+
+ /** An empty SafeHtmlValue instance. */
+ public static final SafeHtmlValue EMPTY = new SafeHtmlValue("");
+
+ /**
+ * Constructor.
+ * @param value the value to be wrapped.
+ */
+ public SafeHtmlValue(String value) {
+ _value = value;
+ }
+
+ /**
+ * Returns the wrapped value.
+ */
+ public String getValue() {
+ return _value;
+ }
+
+ @Override
+ public String toString() {
+ return toJSONString();
+ }
+
+ @Override
+ public String toJSONString() {
+ return JSONValue.toJSONString(_value);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ SafeHtmlValue that = (SafeHtmlValue) o;
+ return Objects.equals(_value, that._value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(_value);
+ }
+
+ /**
+ * Returns a SafeHtmlValue instance for the specified value.
+ * @param value the value to be wrapped.
+ * @since 10.0.0
+ */
+ public static SafeHtmlValue valueOf(String value) {
+ return new SafeHtmlValue(value);
+ }
+}
diff --git a/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java b/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java
index 615233dceaa..7a858246dc5 100644
--- a/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java
+++ b/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java
@@ -23,6 +23,7 @@
import org.zkoss.json.JSONs;
import org.zkoss.lang.Generics;
import org.zkoss.lang.Strings;
+import org.zkoss.xml.XMLs;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
@@ -95,7 +96,7 @@ private String renderValue(String value) {
if (value == null)
return null;
else {
- return Strings.escapeJavaScript(value);
+ return Strings.escapeJavaScript(XMLs.escapeXML(value));
}
}
diff --git a/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java b/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java
index 873a75c461e..1784ec5b948 100644
--- a/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java
+++ b/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java
@@ -25,6 +25,7 @@
import org.zkoss.json.JSONAware;
import org.zkoss.json.JSONs;
import org.zkoss.lang.Strings;
+import org.zkoss.xml.XMLs;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
@@ -153,7 +154,7 @@ private void renderValue(String value) {
_buf.append((String) null);
else {
_buf.append('\'');
- _buf.append(Strings.escapeJavaScript(value));
+ _buf.append(Strings.escapeJavaScript(XMLs.escapeXML(value)));
_buf.append('\'');
}
}
diff --git a/zk/src/main/resources/web/js/zk/widget.ts b/zk/src/main/resources/web/js/zk/widget.ts
index 1aa5a68cced..7e11bc1b2af 100755
--- a/zk/src/main/resources/web/js/zk/widget.ts
+++ b/zk/src/main/resources/web/js/zk/widget.ts
@@ -3160,7 +3160,7 @@ new zul.wnd.Window({
if ((s = this.domClass_(no)))
out += ' class="' + s + '"';
if ((s = this.domTooltiptext_()))
- out += ' title="' + zUtl.encodeXML(s) + '"'; // ZK-676
+ out += ' title="' + s + '"'; // ZK-676
if ((tabIndex = this.getTabindex()) != undefined)
out += ' tabindex="' + tabIndex + '"';
} else {
diff --git a/zkdoc/release-note b/zkdoc/release-note
index 0f2ab30ae37..94f94f70575 100644
--- a/zkdoc/release-note
+++ b/zkdoc/release-note
@@ -7,6 +7,7 @@ ZK 10.0.0
ZK-5048: MVVM DebuggerFactory should log via SLF4J
ZK-5595: Upgrade Servlet version from 2.4 to 3.1 aligned with Java EE 7
ZK-5596: Simplify the JavaScript url when enable embedded mode
+ ZK-5182: Prevent XSS issue in component attributes
* Bugs
ZK-5393: Update ZK jars to jakarta-friendly uploads
@@ -20,6 +21,7 @@ ZK 10.0.0
ZK-5569: Radiogroup onCheck event type mismatch
ZK-5161: page directive's attributes are not encoded before rendering into HTML
ZK-5598: CKEditor requests ExecutionImpl.encodeURL with null value, causes NPE in embedded mode
+ ZK-5162: emptyMessage is not escaped with HTML characters
* Upgrade Notes
+ Upgrade commons-fileupload to commons-fileupload2-javax 2.0.0-M1 and commons-io to 2.13.0 to support jakarta-friendly uploads
@@ -29,6 +31,10 @@ ZK 10.0.0
+ Remove all deprecated Java APIs and the legacy module "zkplus-legacy".
+ Use /zkEmbedded url (or specify yourself) to simply include embedded.js, instead of specify the real path to embedded.js,
simplifying the source to enable embedded mode.
+ + To prevent XSS issue in component attributes, all component attributes will be
+ encoded before rendering into HTML. If you want to render HTML in component attributes,
+ please use the corresponding SafeHtmlValue object APIs instead of using HTML characters.
+ Such as Comboitem, Menu, Navitem, and HTML components.
--------
ZK 10.0.0-Beta
diff --git a/zktest/src/main/webapp/test2/B100-ZK-5162.zul b/zktest/src/main/webapp/test2/B100-ZK-5162.zul
new file mode 100644
index 00000000000..d32f40f8196
--- /dev/null
+++ b/zktest/src/main/webapp/test2/B100-ZK-5162.zul
@@ -0,0 +1,24 @@
+
+
+
+
It is useful to show the description in more versatile way. * - *
Default: empty (""). - * - *
Deriving class can override it to return whatever it wants - * other than null. + * @see #getDescription + * @since 10.0.0 + */ + public SafeHtmlValue getRawContent() { + return _content; + } + + /** Sets the embedded content that is + * shown as part of the description. * *
Unlike other methods, the content assigned to this method - * is generated directly to the browser without escaping. - * Thus, it is better not to have something input by the user to avoid - * any XSS - * attach. + *
Since 10.0.0, the content assigned to this method will be escaped by default. + * To avoid escaping, use {@link #setContent(SafeHtmlValue)} instead. * @see #setDescription * @since 3.0.0 */ public void setContent(String content) { if (content == null) content = ""; + content = Strings.escapeJavaScript(XMLs.escapeXML(content)); + if (!Objects.equals(_content, SafeHtmlValue.valueOf(content))) { + _content = SafeHtmlValue.valueOf(content); + smartUpdate("content", getRawContent()); + } + } + + /** Sets the embedded content (i.e., HTML tags) that is + * shown as part of the description. + * @since 10.0.0 + */ + public void setContent(SafeHtmlValue content) { + if (content == null) + content = SafeHtmlValue.EMPTY; if (!Objects.equals(_content, content)) { _content = content; - smartUpdate("content", getContent()); //allow overriding getContent() + smartUpdate("content", getRawContent()); } } @@ -203,7 +222,7 @@ protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) th render(renderer, "disabled", _disabled); render(renderer, "description", getDescription()); //allow overriding getDescription() - render(renderer, "content", getContent()); //allow overriding getContent() + render(renderer, "content", getRawContent()); //allow overriding getContent() if (_value instanceof String) { render(renderer, "value", _value); diff --git a/zul/src/main/java/org/zkoss/zul/Html.java b/zul/src/main/java/org/zkoss/zul/Html.java index 1b88e765940..2e422537185 100644 --- a/zul/src/main/java/org/zkoss/zul/Html.java +++ b/zul/src/main/java/org/zkoss/zul/Html.java @@ -20,6 +20,7 @@ import org.zkoss.json.JavaScriptValue; import org.zkoss.lang.Objects; +import org.zkoss.zk.ui.SafeHtmlValue; import org.zkoss.zk.ui.sys.HtmlPageRenders; import org.zkoss.zul.impl.XulElement; @@ -72,7 +73,7 @@ * @author tomyeh */ public class Html extends XulElement { - private String _content = ""; + private SafeHtmlValue _content = SafeHtmlValue.EMPTY; /** Constructs a {@link Html} component to embed HTML tags. */ @@ -83,12 +84,19 @@ public Html() { * with the specified content. */ public Html(String content) { - _content = content != null ? content : ""; + _content = SafeHtmlValue.valueOf(content != null ? content : ""); } /** Returns the embedded content (i.e., HTML tags). */ public String getContent() { + return _content.toString(); + } + + /** Returns the embedded content (i.e., HTML tags). + * @since 10.0.0 + */ + public SafeHtmlValue getRawContent() { return _content; } @@ -107,10 +115,9 @@ public String getContent() { public void setContent(String content) { if (content == null) content = ""; - if (!Objects.equals(_content, content)) { - _content = content; - smartUpdate("content", getContent()); - //allow deriving to override getContent() + if (!Objects.equals(_content, SafeHtmlValue.valueOf(content))) { + _content = SafeHtmlValue.valueOf(content); + smartUpdate("content", getRawContent()); } } @@ -135,7 +142,7 @@ protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) th if (cnt == null) renderer.render("content", new JavaScriptValue("zk('" + getUuid() + "').detachChildren()")); else - render(renderer, "content", cnt); + render(renderer, "content", getRawContent()); } } diff --git a/zul/src/main/java/org/zkoss/zul/Menu.java b/zul/src/main/java/org/zkoss/zul/Menu.java index c4d5ff401b0..7353267ea89 100644 --- a/zul/src/main/java/org/zkoss/zul/Menu.java +++ b/zul/src/main/java/org/zkoss/zul/Menu.java @@ -20,9 +20,12 @@ import java.util.Map; import org.zkoss.lang.Objects; +import org.zkoss.lang.Strings; +import org.zkoss.xml.XMLs; import org.zkoss.zk.au.AuRequest; import org.zkoss.zk.au.out.AuInvoke; import org.zkoss.zk.ui.Component; +import org.zkoss.zk.ui.SafeHtmlValue; import org.zkoss.zk.ui.UiException; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.event.InputEvent; @@ -41,7 +44,7 @@ */ public class Menu extends LabelImageElement implements org.zkoss.zk.ui.ext.Disable { private Menupopup _popup; - private String _content = ""; + private SafeHtmlValue _content = SafeHtmlValue.EMPTY; private boolean _disabled = false; static { @@ -83,10 +86,22 @@ public Menupopup getMenupopup() { * @since 5.0.0 */ public String getContent() { + return _content.toString(); + } + + /** Returns the embedded content (i.e., HTML tags) that is + * shown as part of the description. + * + *
It is useful to show the description in more versatile way. + * + * @since 10.0.0 + */ + public SafeHtmlValue getRawContent() { return _content; } - /** Sets the embedded content (i.e., HTML tags) that is + + /** Sets the embedded content that is * shown as part of the description. * *
It is useful to show the description in more versatile way. @@ -94,14 +109,32 @@ public String getContent() { *
There is a way to create Colorbox automatically by using
* #color=#RRGGBB, usage example setContent("#color=#FFFFFF")
*
+ *
Since 10.0.0, the content assigned to this method will be escaped by default.
+ * To avoid escaping, use {@link #setContent(SafeHtmlValue)} instead.
* @since 5.0.0
*/
public void setContent(String content) {
if (content == null)
content = "";
+ content = Strings.escapeJavaScript(XMLs.escapeXML(content));
+ if (!Objects.equals(_content, SafeHtmlValue.valueOf(content))) {
+ _content = SafeHtmlValue.valueOf(content);
+ smartUpdate("content", getRawContent());
+ }
+ }
+
+
+ /** Sets the embedded content (i.e., HTML tags) that is
+ * shown as part of the description.
+ * @since 10.0.0
+ */
+ public void setContent(SafeHtmlValue content) {
+ if (content == null)
+ content = SafeHtmlValue.EMPTY;
if (!Objects.equals(_content, content)) {
_content = content;
- smartUpdate("content", content);
+ smartUpdate("content", getRawContent());
}
}
@@ -141,7 +174,7 @@ public String getZclass() {
protected void renderProperties(ContentRenderer renderer) throws IOException {
super.renderProperties(renderer);
- render(renderer, "content", _content);
+ render(renderer, "content", getRawContent());
render(renderer, "disabled", _disabled);
}
diff --git a/zul/src/main/resources/web/js/zul/inp/InputWidget.ts b/zul/src/main/resources/web/js/zul/inp/InputWidget.ts
index 6a58e49b321..bb5019a7c91 100644
--- a/zul/src/main/resources/web/js/zul/inp/InputWidget.ts
+++ b/zul/src/main/resources/web/js/zul/inp/InputWidget.ts
@@ -629,7 +629,7 @@ export class InputWidget