From 1e555c0b029feee30f44474e7978c3aedda33b17 Mon Sep 17 00:00:00 2001 From: mbiuki Date: Tue, 14 Apr 2026 16:12:05 -0400 Subject: [PATCH 1/4] feat(security): add OWASP Java Encoder and expose via XssWebAPI viewtool (fixes #24120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrates the OWASP Java Encoder (1.3.1) into dotCMS core as the standard context-aware output encoding library for XSS prevention. Changes: - bom/application/pom.xml, dotCMS/pom.xml: add org.owasp.encoder:encoder:1.3.1 - Xss.java: replace StringEscapeUtils.escapeHtml() with Encode.forHtml(); replace UtilMethods.encodeURL() with Encode.forUriComponent(); add new context-specific helpers: encodeForHTML, encodeForHTMLAttribute, encodeForJavaScript, encodeForCSS - VelocityRequestWrapper.java: replace htmlifyString() with Xss.encodeForHTML() in getParameter() for standards-compliant output encoding - XssWebAPI.java: expose all OWASP encoder contexts to Velocity templates via $xsstool — encodeForHTML, encodeForHTMLAttribute, encodeForJavaScript, encodeForURL, encodeForCSS; legacy strip/escape methods kept and deprecated Co-Authored-By: Claude Sonnet 4.6 --- bom/application/pom.xml | 6 + dotCMS/pom.xml | 5 + .../viewtools/VelocityRequestWrapper.java | 2 +- .../velocity/viewtools/XssWebAPI.java | 195 +++++++--- .../src/main/java/com/liferay/util/Xss.java | 333 +++++++++++------- 5 files changed, 351 insertions(+), 190 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 56a601e71844..b086a04be298 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1145,6 +1145,12 @@ + + org.owasp.encoder + encoder + 1.3.1 + + io.jsonwebtoken jjwt diff --git a/dotCMS/pom.xml b/dotCMS/pom.xml index 7d9c83ea2faa..2ed32e6d873d 100644 --- a/dotCMS/pom.xml +++ b/dotCMS/pom.xml @@ -991,6 +991,11 @@ + + org.owasp.encoder + encoder + + io.jsonwebtoken jjwt diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/VelocityRequestWrapper.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/VelocityRequestWrapper.java index 3dfbbe51371b..abb26c65fe2d 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/VelocityRequestWrapper.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/VelocityRequestWrapper.java @@ -83,7 +83,7 @@ public HttpSession getSession(final boolean forceCreation) { public String getParameter(final String param) { String ret = super.getParameter(param); if (UtilMethods.isSet(ret) && Xss.URLHasXSS(ret)) { - ret = UtilMethods.htmlifyString(ret); + ret = Xss.encodeForHTML(ret); } return ret; } diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/XssWebAPI.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/XssWebAPI.java index cc3821557ac9..fe3c35500533 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/XssWebAPI.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/XssWebAPI.java @@ -1,67 +1,150 @@ package com.dotcms.rendering.velocity.viewtools; +import com.liferay.util.Xss; import org.apache.velocity.tools.view.tools.ViewTool; -import com.liferay.util.Xss; +/** + * Velocity view tool ({@code $xsstool}) that exposes context-aware output encoding via the + * OWASP Java Encoder library. + * + *

Use the appropriate method for the output context to prevent XSS: + *

    + *
  • {@link #encodeForHTML(String)} — inside HTML element content
  • + *
  • {@link #encodeForHTMLAttribute(String)} — inside a quoted HTML attribute value
  • + *
  • {@link #encodeForJavaScript(String)} — inside a JavaScript string literal
  • + *
  • {@link #encodeForURL(String)} — inside a URI component (query param, path segment)
  • + *
  • {@link #encodeForCSS(String)} — inside a CSS string or identifier
  • + *
+ * + *

Example usage in a Velocity template: + *

+ *   <p>$xsstool.encodeForHTML($request.getParameter("name"))</p>
+ *   <a href="/search?q=$xsstool.encodeForURL($request.getParameter("q"))">Search</a>
+ *   <script>var msg = "$xsstool.encodeForJavaScript($message)";</script>
+ * 
+ * + *

Registered in {@code toolbox.xml} under the key {@code xsstool}. + * + * @see Xss + */ +public class XssWebAPI implements ViewTool { + + @Override + public void init(Object obj) { + } + + /** + * Encodes the given value for safe inclusion in HTML body content. + * Replaces characters such as {@code <}, {@code >}, {@code &}, {@code "}, and {@code '} + * with their HTML entity equivalents. + * + * @param value the raw value to encode + * @return the HTML-encoded value, or an empty string if {@code value} is null + */ + public String encodeForHTML(final String value) { + return Xss.encodeForHTML(value); + } + + /** + * Encodes the given value for safe inclusion inside a quoted HTML attribute value. + * + * @param value the raw value to encode + * @return the HTML-attribute-encoded value, or an empty string if {@code value} is null + */ + public String encodeForHTMLAttribute(final String value) { + return Xss.encodeForHTMLAttribute(value); + } + + /** + * Encodes the given value for safe embedding inside a JavaScript string literal. + * Use this when rendering user data inside {@code ")); + } + + @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)); + } + +} From 45797c112c6448476a639840a52bb0dadab17a0a Mon Sep 17 00:00:00 2001 From: mbiuki Date: Tue, 14 Apr 2026 17:55:36 -0400 Subject: [PATCH 3/4] feat: add OwaspEncoderTool viewtool and config flag for XSS encoding - Add $encode Velocity viewtool (OwaspEncoderTool) exposing full OWASP Java Encoder API: forHtml, forHtmlContent, forHtmlAttribute, forHtmlUnquotedAttribute, forCssString, forCssUrl, forUriComponent, forJavaScript, forJavaScriptAttribute, forJavaScriptBlock, forJavaScriptSource, forXml*, forCDATA, plus URL safety helpers (validateUrl, urlHasXSS, cleanUrl). Registered as $encode in toolbox.xml. - Wrap VelocityRequestWrapper XSS encoding in USE_OWASP_ENCODING_FOR_XSS_PARAMS config flag (default true) so it can be reverted to legacy htmlifyString if needed. Closes #24120 Co-Authored-By: Claude Sonnet 4.6 --- .../velocity/viewtools/OwaspEncoderTool.java | 307 ++++++++++++++++++ .../viewtools/VelocityRequestWrapper.java | 4 +- dotCMS/src/main/webapp/WEB-INF/toolbox.xml | 5 + 3 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java new file mode 100644 index 000000000000..9787e423c576 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java @@ -0,0 +1,307 @@ +package com.dotcms.rendering.velocity.viewtools; + +import io.vavr.Lazy; +import io.vavr.control.Try; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.commons.validator.routines.UrlValidator; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.velocity.tools.view.tools.ViewTool; +import org.owasp.encoder.Encode; + +/** + * Velocity view tool ({@code $encode}) that exposes the full + * OWASP Java Encoder API + * directly to Velocity templates, covering every output context: HTML, HTML attributes, + * JavaScript, CSS, URI components, and XML. + * + *

Registered in {@code toolbox.xml} under the key {@code encode}. + * + *

Example Velocity usage: + *

+ *   <p>$encode.forHtml($request.getParameter("name"))</p>
+ *   <a href="/search?q=$encode.forUriComponent($request.getParameter("q"))">Go</a>
+ *   <script>var msg = "$encode.forJavaScript($message)";</script>
+ *   <div style="color: $encode.forCssString($color)">...</div>
+ * 
+ * + * @see com.liferay.util.Xss + */ +public class OwaspEncoderTool implements ViewTool { + + private static final Lazy URL_VALIDATOR = + Lazy.of(() -> new UrlValidator(new String[]{"http", "https"})); + + @Override + public void init(final Object obj) { + // no initialisation needed + } + + // ------------------------------------------------------------------------- + // URL safety helpers + // ------------------------------------------------------------------------- + + /** + * Returns {@code true} if the given URL is syntactically valid (http/https only). + * + * @param url the URL to validate + * @return {@code true} if valid + */ + public boolean validateUrl(final String url) { + return URL_VALIDATOR.get().isValid(url); + } + + /** + * Returns {@code true} if any query parameter name or value in the URL contains + * characters that would be altered by HTML-attribute encoding — a strong signal of + * an XSS payload. Returns {@code false} for malformed or non-http(s) URLs. + * + * @param urlToTest the fully-qualified URL to inspect + * @return {@code true} if suspicious content is found in any query parameter + */ + public boolean urlHasXSS(final String urlToTest) { + if (!URL_VALIDATOR.get().isValid(urlToTest)) { + return false; + } + final URL url = Try.of(() -> new URL(urlToTest)).getOrNull(); + if (url == null) { + return true; + } + final List 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)); + } +}