params =
+ URLEncodedUtils.parse(url.getQuery(), StandardCharsets.UTF_8);
+ return params.stream().parallel().anyMatch(p ->
+ (p.getName() != null && !p.getName().equals(forHtmlAttribute(p.getName())))
+ || (p.getValue() != null && !p.getValue().equals(forHtmlAttribute(p.getValue()))));
+ }
+
+ /**
+ * Returns the URL HTML-attribute-encoded if it is valid, or {@code null} if it fails
+ * validation. Use this when outputting a URL inside an HTML attribute value.
+ *
+ * @param url the URL to clean
+ * @return the encoded URL, or {@code null}
+ */
+ public String cleanUrl(final String url) {
+ if (URL_VALIDATOR.get().isValid(url)) {
+ return forHtmlAttribute(url);
+ }
+ return null;
+ }
+
+ // -------------------------------------------------------------------------
+ // HTML encoding
+ // -------------------------------------------------------------------------
+
+ /**
+ * Encodes for (X)HTML text content and quoted attribute values.
+ * Prefer the more specific {@link #forHtmlContent(String)} or
+ * {@link #forHtmlAttribute(String)} when the context is known.
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forHtml(final String input) {
+ return input != null ? Encode.forHtml(input) : "";
+ }
+
+ /**
+ * Encodes for HTML text content (inside an element, not inside an attribute).
+ * Does not encode single or double quotes.
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forHtmlContent(final String input) {
+ return input != null ? Encode.forHtmlContent(input) : "";
+ }
+
+ /**
+ * Encodes for a quoted HTML attribute value (both single- and double-quoted).
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forHtmlAttribute(final String input) {
+ return input != null ? Encode.forHtmlAttribute(input) : "";
+ }
+
+ /**
+ * Encodes for an unquoted HTML attribute value.
+ * Prefer {@link #forHtmlAttribute(String)} for quoted attributes.
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forHtmlUnquotedAttribute(final String input) {
+ return input != null ? Encode.forHtmlUnquotedAttribute(input) : "";
+ }
+
+ // -------------------------------------------------------------------------
+ // CSS encoding
+ // -------------------------------------------------------------------------
+
+ /**
+ * Encodes for a CSS string literal (must be surrounded by quotation characters).
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forCssString(final String input) {
+ return input != null ? Encode.forCssString(input) : "";
+ }
+
+ /**
+ * Encodes for a CSS {@code url()} context (must be surrounded by {@code url(} / {@code )}).
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forCssUrl(final String input) {
+ return input != null ? Encode.forCssUrl(input) : "";
+ }
+
+ // -------------------------------------------------------------------------
+ // URI encoding
+ // -------------------------------------------------------------------------
+
+ /**
+ * Percent-encodes a URI component (query parameter name/value, path segment, etc.).
+ * This is the preferred method for embedding user data in URL query strings.
+ *
+ * @param input the raw value to encode
+ * @return the percent-encoded value, or an empty string if {@code input} is null
+ */
+ public String forUriComponent(final String input) {
+ return input != null ? Encode.forUriComponent(input) : "";
+ }
+
+ /**
+ * Percent-encodes a full URI according to RFC 3986.
+ * Note: a {@code javascript:} URI provided by a user would still pass through.
+ * Prefer {@link #forUriComponent(String)} for individual components.
+ *
+ * @param input the raw URI to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ * @deprecated Use {@link #forUriComponent(String)} for URI components.
+ */
+ @Deprecated
+ public String forUri(final String input) {
+ return input != null ? Encode.forUri(input) : "";
+ }
+
+ // -------------------------------------------------------------------------
+ // JavaScript encoding
+ // -------------------------------------------------------------------------
+
+ /**
+ * Encodes for a JavaScript string literal. Safe in script blocks, HTML event attributes,
+ * and JSON files. The caller must supply surrounding quotation characters.
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forJavaScript(final String input) {
+ return input != null ? Encode.forJavaScript(input) : "";
+ }
+
+ /**
+ * Encodes for a JavaScript inline event attribute (e.g. {@code onclick="..."}).
+ *
+ * @param input the raw value to encode
+ * @return the encoded value, or an empty string if {@code input} is null
+ */
+ public String forJavaScriptAttribute(final String input) {
+ return input != null ? Encode.forJavaScriptAttribute(input) : "";
+ }
+
+ /**
+ * Encodes for a JavaScript {@code "));
+ }
+
+ @Test
+ public void forHtml_encodesAmpersand() {
+ assertTrue(tool.forHtml("a & b").contains("&"));
+ }
+
+ @Test
+ public void forHtml_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtml(null));
+ }
+
+ @Test
+ public void forHtml_passesThroughPlainText() {
+ assertEquals("Hello World", tool.forHtml("Hello World"));
+ }
+
+ // -------------------------------------------------------------------------
+ // forHtmlContent
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forHtmlContent_encodesAngleBrackets() {
+ final String result = tool.forHtmlContent("bold");
+ assertFalse("Raw angle bracket must not appear", result.contains(""));
+ }
+
+ @Test
+ public void forHtmlContent_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtmlContent(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forHtmlAttribute
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forHtmlAttribute_encodesDoubleQuote() {
+ final String result = tool.forHtmlAttribute("\" onmouseover=\"alert(1)");
+ assertFalse("Unencoded double-quote must not appear", result.contains("\""));
+ }
+
+ @Test
+ public void forHtmlAttribute_encodesSingleQuote() {
+ final String result = tool.forHtmlAttribute("' onmouseover='alert(1)");
+ assertFalse("Unencoded single-quote must not appear", result.contains("'"));
+ }
+
+ @Test
+ public void forHtmlAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtmlAttribute(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forHtmlUnquotedAttribute
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forHtmlUnquotedAttribute_encodesSpaceAndQuotes() {
+ final String result = tool.forHtmlUnquotedAttribute("value with spaces");
+ assertFalse("Space must be encoded for unquoted attribute", result.contains(" "));
+ }
+
+ @Test
+ public void forHtmlUnquotedAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtmlUnquotedAttribute(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forCssString
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forCssString_encodesSingleQuote() {
+ final String result = tool.forCssString("'; } body { color: red; x: '");
+ assertFalse("Single quote must be encoded for CSS string breakout prevention",
+ result.contains("'"));
+ }
+
+ @Test
+ public void forCssString_returnsEmptyStringForNull() {
+ assertEquals("", tool.forCssString(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forCssUrl
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forCssUrl_encodesQuotes() {
+ final String result = tool.forCssUrl("'malicious'");
+ assertFalse("Single quote must be encoded in CSS URL context", result.contains("'"));
+ }
+
+ @Test
+ public void forCssUrl_returnsEmptyStringForNull() {
+ assertEquals("", tool.forCssUrl(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forUriComponent
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forUriComponent_encodesSpaceAndSpecialChars() {
+ final String result = tool.forUriComponent("hello world & more");
+ assertFalse("Space must be percent-encoded", result.contains(" "));
+ assertFalse("Ampersand must be percent-encoded", result.contains("&"));
+ }
+
+ @Test
+ public void forUriComponent_encodesAngleBrackets() {
+ final String result = tool.forUriComponent("");
+ assertFalse("Angle bracket must be percent-encoded", result.contains("<"));
+ }
+
+ @Test
+ public void forUriComponent_preservesUnreservedChars() {
+ final String safe = "hello-world_123~";
+ assertEquals("Unreserved URI chars must not be encoded", safe, tool.forUriComponent(safe));
+ }
+
+ @Test
+ public void forUriComponent_returnsEmptyStringForNull() {
+ assertEquals("", tool.forUriComponent(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forJavaScript
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forJavaScript_encodesSingleQuote() {
+ final String result = tool.forJavaScript("'; alert(1); var x='");
+ assertFalse("Single quote must be encoded for JS string breakout prevention",
+ result.contains("'"));
+ }
+
+ @Test
+ public void forJavaScript_encodesBackslash() {
+ final String result = tool.forJavaScript("back\\slash");
+ assertTrue("Backslash must be doubled in JS output", result.contains("\\\\"));
+ }
+
+ @Test
+ public void forJavaScript_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScript(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forJavaScriptAttribute / forJavaScriptBlock / forJavaScriptSource
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forJavaScriptAttribute_encodesSingleQuote() {
+ assertFalse(tool.forJavaScriptAttribute("'").contains("'"));
+ }
+
+ @Test
+ public void forJavaScriptAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScriptAttribute(null));
+ }
+
+ @Test
+ public void forJavaScriptBlock_encodesScriptCloseTag() {
+ // Inside a , not single quotes.
+ // OWASP encodes the '<' to prevent the HTML parser closing the script early.
+ final String result = tool.forJavaScriptBlock("");
+ assertFalse(" breakout must be prevented in script-block context",
+ result.contains(""));
+ }
+
+ @Test
+ public void forJavaScriptBlock_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScriptBlock(null));
+ }
+
+ @Test
+ public void forJavaScriptSource_encodesBackslash() {
+ // In a standalone .js file, backslash is the relevant encoding target.
+ final String result = tool.forJavaScriptSource("back\\slash");
+ assertTrue("Backslash must be doubled in JS source output", result.contains("\\\\"));
+ }
+
+ @Test
+ public void forJavaScriptSource_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScriptSource(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forXml family
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forXml_encodesAngleBrackets() {
+ final String result = tool.forXml("");
+ assertFalse("Angle bracket must be encoded", result.contains(""));
+ assertTrue(result.contains("<"));
+ }
+
+ @Test
+ public void forXml_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXml(null));
+ }
+
+ @Test
+ public void forXmlContent_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXmlContent(null));
+ }
+
+ @Test
+ public void forXmlAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXmlAttribute(null));
+ }
+
+ @Test
+ public void forXmlComment_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXmlComment(null));
+ }
+
+ @Test
+ public void forCDATA_returnsEmptyStringForNull() {
+ assertEquals("", tool.forCDATA(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forJava
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void forJava_encodesBackslash() {
+ final String result = tool.forJava("back\\slash");
+ assertTrue("Backslash must be doubled in Java string output", result.contains("\\\\"));
+ }
+
+ @Test
+ public void forJava_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJava(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // URL safety helpers — validateUrl
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void validateUrl_acceptsValidHttpsUrl() {
+ assertTrue(tool.validateUrl("https://www.dotcms.com/page?q=1"));
+ }
+
+ @Test
+ public void validateUrl_rejectsJavascriptScheme() {
+ assertFalse(tool.validateUrl("javascript:alert(1)"));
+ }
+
+ @Test
+ public void validateUrl_rejectsNull() {
+ assertFalse(tool.validateUrl(null));
+ }
+
+ @Test
+ public void validateUrl_rejectsMalformed() {
+ assertFalse(tool.validateUrl("not a url"));
+ }
+
+ // -------------------------------------------------------------------------
+ // URL safety helpers — urlHasXSS
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void urlHasXSS_returnsFalseForCleanUrl() {
+ assertFalse(tool.urlHasXSS("https://www.dotcms.com/page?name=hello"));
+ }
+
+ @Test
+ public void urlHasXSS_returnsTrueWhenParamContainsHtmlTags() {
+ assertTrue(tool.urlHasXSS("https://www.dotcms.com/page?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E"));
+ }
+
+ @Test
+ public void urlHasXSS_returnsFalseForInvalidUrl() {
+ assertFalse(tool.urlHasXSS("not a url"));
+ }
+
+ @Test
+ public void urlHasXSS_returnsFalseForNull() {
+ assertFalse(tool.urlHasXSS(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // URL safety helpers — cleanUrl
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void cleanUrl_returnsEncodedUrlForValidInput() {
+ final String result = tool.cleanUrl("https://www.dotcms.com/page");
+ assertEquals("https://www.dotcms.com/page", result);
+ }
+
+ @Test
+ public void cleanUrl_returnsNullForInvalidUrl() {
+ assertNull(tool.cleanUrl("javascript:alert(1)"));
+ }
+
+ @Test
+ public void cleanUrl_returnsNullForNull() {
+ assertNull(tool.cleanUrl(null));
+ }
+}
diff --git a/dotCMS/src/test/java/com/liferay/util/XssTest.java b/dotCMS/src/test/java/com/liferay/util/XssTest.java
new file mode 100644
index 000000000000..20452bf42d94
--- /dev/null
+++ b/dotCMS/src/test/java/com/liferay/util/XssTest.java
@@ -0,0 +1,186 @@
+package com.liferay.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link Xss} encoding methods.
+ *
+ * Verifies that each context-specific encoder (HTML, HTML attribute, JavaScript, URL, CSS)
+ * correctly encodes dangerous characters and that null inputs return empty strings rather than
+ * throwing.
+ */
+public class XssTest {
+
+ // -------------------------------------------------------------------------
+ // encodeForHTML
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void encodeForHTML_encodesScriptTag() {
+ assertEquals("<script>alert(1)</script>",
+ Xss.encodeForHTML(""));
+ }
+
+ @Test
+ public void encodeForHTML_encodesAmpersandAndQuotes() {
+ final String result = Xss.encodeForHTML("a & b
");
+ // OWASP encoder uses " (numeric) for double-quotes and & for ampersands — both valid HTML
+ assertFalse("Raw angle bracket must not appear", result.contains("alert(1)");
+ assertFalse("Angle brackets must be percent-encoded", result.contains("<"));
+ assertFalse("Angle brackets must be percent-encoded", result.contains(">"));
+ }
+
+ @Test
+ public void encodeForURL_returnsEmptyStringForNull() {
+ assertEquals("", Xss.encodeForURL(null));
+ }
+
+ @Test
+ public void encodeForURL_preservesUnreservedCharacters() {
+ final String safe = "hello-world_123~";
+ assertEquals("Unreserved URI chars must not be encoded", safe, Xss.encodeForURL(safe));
+ }
+
+ // -------------------------------------------------------------------------
+ // encodeForCSS
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void encodeForCSS_encodesQuotesAndParens() {
+ // Inside a CSS string literal, single/double quotes and parens are breakout vectors
+ final String input = "'; } body { background: red; x: '";
+ final String result = Xss.encodeForCSS(input);
+ assertFalse("Single quote must be encoded to prevent CSS string breakout",
+ result.contains("'"));
+ }
+
+ @Test
+ public void encodeForCSS_returnsEmptyStringForNull() {
+ assertEquals("", Xss.encodeForCSS(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // escapeHTMLAttrib (legacy — delegates to encodeForHTML)
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void escapeHTMLAttrib_encodesHtmlEntities() {
+ assertEquals("<b>bold</b>", Xss.escapeHTMLAttrib("bold"));
+ }
+
+ @Test
+ public void escapeHTMLAttrib_returnsEmptyStringForNull() {
+ assertEquals("", Xss.escapeHTMLAttrib(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // unEscapeHTMLAttrib
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void unEscapeHTMLAttrib_decodesHtmlEntities() {
+ assertEquals("bold", Xss.unEscapeHTMLAttrib("<b>bold</b>"));
+ }
+
+ @Test
+ public void unEscapeHTMLAttrib_returnsEmptyStringForNull() {
+ assertEquals("", Xss.unEscapeHTMLAttrib(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // URLHasXSS / URIHasXSS
+ // -------------------------------------------------------------------------
+
+ @Test
+ public void URLHasXSS_detectsScriptTag() {
+ assertTrue(Xss.URLHasXSS(""));
+ }
+
+ @Test
+ public void URLHasXSS_returnsFalseForCleanInput() {
+ assertFalse(Xss.URLHasXSS("hello world"));
+ }
+
+ @Test
+ public void URLHasXSS_returnsFalseForNull() {
+ assertFalse(Xss.URLHasXSS(null));
+ }
+
+}