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 "));
+ }
+
+ /**
+ * Given an input containing an ampersand,
+ * When encoded for HTML body content,
+ * Then the ampersand is replaced with {@code &}.
+ */
+ @Test
+ public void forHtml_encodesAmpersand() {
+ assertTrue(tool.forHtml("a & b").contains("&"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for HTML body content,
+ * Then an empty string is returned instead of throwing.
+ */
+ @Test
+ public void forHtml_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtml(null));
+ }
+
+ /**
+ * Given plain text with no special characters,
+ * When encoded for HTML body content,
+ * Then the value is returned unchanged.
+ */
+ @Test
+ public void forHtml_passesThroughPlainText() {
+ assertEquals("Hello World", tool.forHtml("Hello World"));
+ }
+
+ // -------------------------------------------------------------------------
+ // forHtmlContent
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing angle brackets,
+ * When encoded for HTML text content (element body, not attribute),
+ * Then raw angle brackets do not appear in the output.
+ */
+ @Test
+ public void forHtmlContent_encodesAngleBrackets() {
+ final String result = tool.forHtmlContent("bold");
+ assertFalse("Raw angle bracket must not appear", result.contains(""));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for HTML text content,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forHtmlContent_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtmlContent(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forHtmlAttribute
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing a double-quote followed by an event handler payload,
+ * When encoded for a quoted HTML attribute value,
+ * Then the double-quote is encoded so the attribute cannot be broken out of.
+ */
+ @Test
+ public void forHtmlAttribute_encodesDoubleQuote() {
+ final String result = tool.forHtmlAttribute("\" onmouseover=\"alert(1)");
+ assertFalse("Unencoded double-quote must not appear", result.contains("\""));
+ }
+
+ /**
+ * Given an input containing a single-quote followed by an event handler payload,
+ * When encoded for a quoted HTML attribute value,
+ * Then the single-quote is encoded so the attribute cannot be broken out of.
+ */
+ @Test
+ public void forHtmlAttribute_encodesSingleQuote() {
+ final String result = tool.forHtmlAttribute("' onmouseover='alert(1)");
+ assertFalse("Unencoded single-quote must not appear", result.contains("'"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for an HTML attribute,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forHtmlAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtmlAttribute(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forHtmlUnquotedAttribute
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing spaces,
+ * When encoded for an unquoted HTML attribute value,
+ * Then spaces are encoded so they cannot delimit a new attribute.
+ */
+ @Test
+ public void forHtmlUnquotedAttribute_encodesSpaceAndQuotes() {
+ final String result = tool.forHtmlUnquotedAttribute("value with spaces");
+ assertFalse("Space must be encoded for unquoted attribute", result.contains(" "));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for an unquoted HTML attribute,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forHtmlUnquotedAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forHtmlUnquotedAttribute(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forCssString
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing a single-quote CSS breakout payload,
+ * When encoded for a CSS string literal (surrounded by quotes),
+ * Then the single-quote is encoded so the string cannot be escaped.
+ */
+ @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("'"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a CSS string,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forCssString_returnsEmptyStringForNull() {
+ assertEquals("", tool.forCssString(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forCssUrl
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing single-quotes,
+ * When encoded for a CSS url() context,
+ * Then the single-quotes are encoded.
+ */
+ @Test
+ public void forCssUrl_encodesQuotes() {
+ final String result = tool.forCssUrl("'malicious'");
+ assertFalse("Single quote must be encoded in CSS URL context", result.contains("'"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a CSS url(),
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forCssUrl_returnsEmptyStringForNull() {
+ assertEquals("", tool.forCssUrl(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forUriComponent
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing spaces and ampersands,
+ * When encoded for a URI component (query param value, path segment),
+ * Then both characters are percent-encoded so they cannot break the URL structure.
+ */
+ @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("&"));
+ }
+
+ /**
+ * Given an input containing angle brackets (a script tag),
+ * When encoded for a URI component,
+ * Then angle brackets are percent-encoded.
+ */
+ @Test
+ public void forUriComponent_encodesAngleBrackets() {
+ final String result = tool.forUriComponent("");
+ assertFalse("Angle bracket must be percent-encoded", result.contains("<"));
+ }
+
+ /**
+ * Given an input consisting entirely of unreserved URI characters (letters, digits, {@code -._~}),
+ * When encoded for a URI component,
+ * Then the value is returned unchanged (no over-encoding).
+ */
+ @Test
+ public void forUriComponent_preservesUnreservedChars() {
+ final String safe = "hello-world_123~";
+ assertEquals("Unreserved URI chars must not be encoded", safe, tool.forUriComponent(safe));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a URI component,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forUriComponent_returnsEmptyStringForNull() {
+ assertEquals("", tool.forUriComponent(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forJavaScript
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing a single-quote JS string breakout payload,
+ * When encoded for a JavaScript string literal,
+ * Then the single-quote is encoded so the string cannot be terminated early.
+ */
+ @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("'"));
+ }
+
+ /**
+ * Given an input containing a backslash,
+ * When encoded for a JavaScript string literal,
+ * Then the backslash is doubled so it cannot act as an escape character.
+ */
+ @Test
+ public void forJavaScript_encodesBackslash() {
+ final String result = tool.forJavaScript("back\\slash");
+ assertTrue("Backslash must be doubled in JS output", result.contains("\\\\"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for JavaScript,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forJavaScript_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScript(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forJavaScriptAttribute / forJavaScriptBlock / forJavaScriptSource
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing a single-quote,
+ * When encoded for an inline HTML event attribute (e.g. {@code onclick='...'}),
+ * Then the single-quote is encoded.
+ */
+ @Test
+ public void forJavaScriptAttribute_encodesSingleQuote() {
+ assertFalse(tool.forJavaScriptAttribute("'").contains("'"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a JavaScript event attribute,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forJavaScriptAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScriptAttribute(null));
+ }
+
+ /**
+ * Given an input containing a {@code } closing tag,
+ * When encoded for a JavaScript {@code ");
+ assertFalse(" breakout must be prevented in script-block context",
+ result.contains(""));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a JavaScript script block,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forJavaScriptBlock_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScriptBlock(null));
+ }
+
+ /**
+ * Given an input containing a backslash,
+ * When encoded for a standalone JavaScript source file,
+ * Then the backslash is doubled.
+ * Note: single-quotes and angle brackets are safe in a pure JS source context (no HTML parser).
+ */
+ @Test
+ public void forJavaScriptSource_encodesBackslash() {
+ final String result = tool.forJavaScriptSource("back\\slash");
+ assertTrue("Backslash must be doubled in JS source output", result.contains("\\\\"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a JavaScript source file,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forJavaScriptSource_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJavaScriptSource(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forXml family
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an XML element string with angle brackets,
+ * When encoded for XML/XHTML content,
+ * Then the angle brackets are replaced with HTML entities.
+ */
+ @Test
+ public void forXml_encodesAngleBrackets() {
+ final String result = tool.forXml("");
+ assertFalse("Angle bracket must be encoded", result.contains(""));
+ assertTrue(result.contains("<"));
+ }
+
+ /** Given a null input, When encoded for XML, Then an empty string is returned. */
+ @Test
+ public void forXml_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXml(null));
+ }
+
+ /** Given a null input, When encoded for XML content, Then an empty string is returned. */
+ @Test
+ public void forXmlContent_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXmlContent(null));
+ }
+
+ /** Given a null input, When encoded for an XML attribute, Then an empty string is returned. */
+ @Test
+ public void forXmlAttribute_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXmlAttribute(null));
+ }
+
+ /** Given a null input, When encoded for an XML comment, Then an empty string is returned. */
+ @Test
+ public void forXmlComment_returnsEmptyStringForNull() {
+ assertEquals("", tool.forXmlComment(null));
+ }
+
+ /** Given a null input, When encoded for a CDATA section, Then an empty string is returned. */
+ @Test
+ public void forCDATA_returnsEmptyStringForNull() {
+ assertEquals("", tool.forCDATA(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // forJava
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing a backslash,
+ * When encoded for a Java string literal,
+ * Then the backslash is doubled.
+ */
+ @Test
+ public void forJava_encodesBackslash() {
+ final String result = tool.forJava("back\\slash");
+ assertTrue("Backslash must be doubled in Java string output", result.contains("\\\\"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a Java string literal,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void forJava_returnsEmptyStringForNull() {
+ assertEquals("", tool.forJava(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // URL safety helpers — validateUrl
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given a well-formed https URL,
+ * When validated,
+ * Then {@code true} is returned.
+ */
+ @Test
+ public void validateUrl_acceptsValidHttpsUrl() {
+ assertTrue(tool.validateUrl("https://www.dotcms.com/page?q=1"));
+ }
+
+ /**
+ * Given a {@code javascript:} URI,
+ * When validated (only http/https are accepted),
+ * Then {@code false} is returned.
+ */
+ @Test
+ public void validateUrl_rejectsJavascriptScheme() {
+ assertFalse(tool.validateUrl("javascript:alert(1)"));
+ }
+
+ /**
+ * Given a null input,
+ * When validated,
+ * Then {@code false} is returned.
+ */
+ @Test
+ public void validateUrl_rejectsNull() {
+ assertFalse(tool.validateUrl(null));
+ }
+
+ /**
+ * Given a malformed string that is not a URL,
+ * When validated,
+ * Then {@code false} is returned.
+ */
+ @Test
+ public void validateUrl_rejectsMalformed() {
+ assertFalse(tool.validateUrl("not a url"));
+ }
+
+ // -------------------------------------------------------------------------
+ // URL safety helpers — urlHasXSS
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given a URL whose query parameters contain only safe values,
+ * When checked for XSS,
+ * Then {@code false} is returned.
+ */
+ @Test
+ public void urlHasXSS_returnsFalseForCleanUrl() {
+ assertFalse(tool.urlHasXSS("https://www.dotcms.com/page?name=hello"));
+ }
+
+ /**
+ * Given a URL whose query parameter value contains a percent-encoded {@code "));
+ }
+
+ /**
+ * Given an input containing angle brackets, double-quotes, and an ampersand,
+ * When encoded for HTML body content,
+ * Then none of those raw characters appear in the output.
+ */
+ @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(">"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for a URI component,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void encodeForURL_returnsEmptyStringForNull() {
+ assertEquals("", Xss.encodeForURL(null));
+ }
+
+ /**
+ * Given unreserved URI characters (letters, digits, {@code -._~}),
+ * When encoded for a URI component,
+ * Then the value is returned unchanged (no over-encoding).
+ */
+ @Test
+ public void encodeForURL_preservesUnreservedCharacters() {
+ final String safe = "hello-world_123~";
+ assertEquals("Unreserved URI chars must not be encoded", safe, Xss.encodeForURL(safe));
+ }
+
+ // -------------------------------------------------------------------------
+ // encodeForCSS
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input containing a single-quote CSS string breakout payload,
+ * When encoded for a CSS string literal,
+ * Then the single-quote is encoded.
+ */
+ @Test
+ public void encodeForCSS_encodesQuotesAndParens() {
+ 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("'"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded for CSS,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void encodeForCSS_returnsEmptyStringForNull() {
+ assertEquals("", Xss.encodeForCSS(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // escapeHTMLAttrib (legacy — delegates to encodeForHTML)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an input with HTML tags,
+ * When encoded via the legacy {@code escapeHTMLAttrib} method,
+ * Then HTML entities are produced (delegates to encodeForHTML).
+ */
+ @Test
+ public void escapeHTMLAttrib_encodesHtmlEntities() {
+ assertEquals("<b>bold</b>", Xss.escapeHTMLAttrib("bold"));
+ }
+
+ /**
+ * Given a null input,
+ * When encoded via the legacy method,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void escapeHTMLAttrib_returnsEmptyStringForNull() {
+ assertEquals("", Xss.escapeHTMLAttrib(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // unEscapeHTMLAttrib
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given an HTML-encoded string,
+ * When decoded,
+ * Then the original plain-text value is restored.
+ */
+ @Test
+ public void unEscapeHTMLAttrib_decodesHtmlEntities() {
+ assertEquals("bold", Xss.unEscapeHTMLAttrib("<b>bold</b>"));
+ }
+
+ /**
+ * Given a null input,
+ * When decoded,
+ * Then an empty string is returned.
+ */
+ @Test
+ public void unEscapeHTMLAttrib_returnsEmptyStringForNull() {
+ assertEquals("", Xss.unEscapeHTMLAttrib(null));
+ }
+
+ // -------------------------------------------------------------------------
+ // urlHasXSS / uriHasXSS (camelCase — canonical names)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Given a string containing a script tag,
+ * When checked for XSS patterns,
+ * Then {@code true} is returned.
+ */
+ @Test
+ public void urlHasXSS_detectsScriptTag() {
+ assertTrue(Xss.urlHasXSS(""));
+ }
+
+ /**
+ * Given a plain string with no XSS patterns,
+ * When checked for XSS,
+ * Then {@code false} is returned.
+ */
+ @Test
+ public void urlHasXSS_returnsFalseForCleanInput() {
+ assertFalse(Xss.urlHasXSS("hello world"));
+ }
+
+ /**
+ * Given a null input,
+ * When checked for XSS,
+ * Then {@code false} is returned (no exception).
+ */
+ @Test
+ public void urlHasXSS_returnsFalseForNull() {
+ assertFalse(Xss.urlHasXSS(null));
+ }
+
+}