Use response encoding when escaping HTML

With SPR-9293, it is now possible to HTML escape text while taking into
account the current response encoding. When using UTF-* encodings, only
XML markup significant characters are escaped, since UTF-* natively
support those characters.

This commit adds a new servlet context parameter to enable this fix by
default in a Spring MVC application:

    <context-param>
      <param-name>responseEncodedHtmlEscape</param-name>
      <param-value>true</param-value>
    </context-param>

Issue: SPR-12350, SPR-12132
This commit is contained in:
Brian Clozel 2014-10-24 11:46:09 +02:00
parent 73e398a165
commit a0c210457b
9 changed files with 124 additions and 17 deletions

View File

@ -106,6 +106,12 @@ public abstract class WebUtils {
*/
public static final String HTML_ESCAPE_CONTEXT_PARAM = "defaultHtmlEscape";
/**
* Use of response encoding for HTML escaping parameter at the servlet context level
* (i.e. a context-param in {@code web.xml}): "responseEncodedHtmlEscape".
*/
public static final String RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM = "responseEncodedHtmlEscape";
/**
* Web app root key parameter at the servlet context level
* (i.e. a context-param in {@code web.xml}): "webAppRootKey".
@ -175,7 +181,9 @@ public abstract class WebUtils {
* (if any). Falls back to {@code false} in case of no explicit default given.
* @param servletContext the servlet context of the web application
* @return whether default HTML escaping is enabled (default is false)
* @deprecated as of Spring 4.1, in favor of {@link #getDefaultHtmlEscape}
*/
@Deprecated
public static boolean isDefaultHtmlEscape(ServletContext servletContext) {
if (servletContext == null) {
return false;
@ -202,6 +210,26 @@ public abstract class WebUtils {
return (StringUtils.hasText(param)? Boolean.valueOf(param) : null);
}
/**
* Return whether response encoding should be used when HTML escaping characters,
* thus only escaping XML markup significant characters with UTF-* encodings.
* This option is enabled for the web application with a ServletContext param,
* i.e. the value of the "responseEncodedHtmlEscape" context-param in {@code web.xml}
* (if any).
* <p>This method differentiates between no param specified at all and
* an actual boolean value specified, allowing to have a context-specific
* default in case of no setting at the global level.
* @param servletContext the servlet context of the web application
* @return whether response encoding is used for HTML escaping (null = no explicit default)
*/
public static Boolean getResponseEncodedHtmlEscape(ServletContext servletContext) {
if (servletContext == null) {
return null;
}
String param = servletContext.getInitParameter(RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM);
return (StringUtils.hasText(param)? Boolean.valueOf(param) : null);
}
/**
* Return the temporary directory for the current web application,
* as provided by the servlet container.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -115,6 +115,8 @@ public class RequestContext {
private Boolean defaultHtmlEscape;
private Boolean responseEncodedHtmlEscape;
private UrlPathHelper urlPathHelper;
private RequestDataValueProcessor requestDataValueProcessor;
@ -263,6 +265,8 @@ public class RequestContext {
// context-param in web.xml, if any.
this.defaultHtmlEscape = WebUtils.getDefaultHtmlEscape(this.webApplicationContext.getServletContext());
this.responseEncodedHtmlEscape = WebUtils.getResponseEncodedHtmlEscape(this.webApplicationContext.getServletContext());
this.urlPathHelper = new UrlPathHelper();
if (this.webApplicationContext.containsBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) {
@ -484,6 +488,27 @@ public class RequestContext {
return this.defaultHtmlEscape;
}
/**
* Is HTML escaping using the response encoding by default?
* If enabled, only XML markup significant characters will be escaped with UTF-* encodings.
* <p>Falls back to {@code false} in case of no explicit default given.
* @since 4.1.2
*/
public boolean isResponseEncodedHtmlEscape() {
return (this.responseEncodedHtmlEscape != null && this.responseEncodedHtmlEscape.booleanValue());
}
/**
* Return the default setting about use of response encoding for HTML escape setting,
* differentiating between no default specified and an explicit value.
* @return whether default use of response encoding HTML escaping is enabled (null = no explicit default)
* @since 4.1.2
*/
public Boolean getResponseEncodedHtmlEscape() {
return this.responseEncodedHtmlEscape;
}
/**
* Set the UrlPathHelper to use for context path and request URI decoding.
* Can be used to pass a shared UrlPathHelper instance in.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,7 +21,6 @@ import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyContent;
import javax.servlet.jsp.tagext.BodyTag;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.JavaScriptUtils;
/**
@ -79,7 +78,7 @@ public class EscapeBodyTag extends HtmlEscapingAwareTag implements BodyTag {
try {
String content = readBodyContent();
// HTML and/or JavaScript escape, if demanded
content = isHtmlEscape() ? HtmlUtils.htmlEscape(content) : content;
content = htmlEscape(content);
content = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(content) : content;
writeBodyContent(content);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,7 +34,6 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.util.ObjectUtils;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.JavaScriptUtils;
import org.springframework.web.util.TagUtils;
@ -121,7 +120,7 @@ public class EvalTag extends HtmlEscapingAwareTag {
try {
String result = this.expression.getValue(evaluationContext, String.class);
result = ObjectUtils.getDisplayString(result);
result = (isHtmlEscape() ? HtmlUtils.htmlEscape(result) : result);
result = htmlEscape(result);
result = (this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(result) : result);
this.pageContext.getOut().print(result);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,6 +18,8 @@ package org.springframework.web.servlet.tags;
import javax.servlet.jsp.JspException;
import org.springframework.web.util.HtmlUtils;
/**
* Superclass for tags that output content that might get HTML-escaped.
*
@ -31,7 +33,8 @@ import javax.servlet.jsp.JspException;
* @see #setHtmlEscape
* @see HtmlEscapeTag
* @see org.springframework.web.servlet.support.RequestContext#isDefaultHtmlEscape
* @see org.springframework.web.util.WebUtils#isDefaultHtmlEscape
* @see org.springframework.web.util.WebUtils#getDefaultHtmlEscape
* @see org.springframework.web.util.WebUtils#getResponseEncodedHtmlEscape
*/
@SuppressWarnings("serial")
public abstract class HtmlEscapingAwareTag extends RequestContextAwareTag {
@ -72,4 +75,37 @@ public abstract class HtmlEscapingAwareTag extends RequestContextAwareTag {
return getRequestContext().isDefaultHtmlEscape();
}
/**
* Return the applicable default for the use of response encoding with HTML escape for this tag.
* <p>The default implementation checks the RequestContext's setting,
* falling back to {@code false} in case of no explicit default given.
* @see #getRequestContext()
* @since 4.1.2
*/
protected boolean isResponseEncodedHtmlEscape() {
return getRequestContext().isResponseEncodedHtmlEscape();
}
/**
* HTML encodes the given string, only if the htmlEscape setting is enabled.
* The response encoding will be taken into account if the responseEncodedHtmlEscape setting is enabled.
* @param content
* @return
* @see #isHtmlEscape()
* @see #isResponseEncodedHtmlEscape()
* @since 4.1.2
*/
protected String htmlEscape(String content) {
String out = content;
if(isHtmlEscape()) {
if(isResponseEncodedHtmlEscape()) {
out = HtmlUtils.htmlEscape(content, this.pageContext.getResponse().getCharacterEncoding());
}
else {
out = HtmlUtils.htmlEscape(content);
}
}
return out;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,7 +28,6 @@ import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.JavaScriptUtils;
import org.springframework.web.util.TagUtils;
@ -181,7 +180,7 @@ public class MessageTag extends HtmlEscapingAwareTag implements ArgumentAware {
String msg = resolveMessage();
// HTML and/or JavaScript escape, if demanded.
msg = isHtmlEscape() ? HtmlUtils.htmlEscape(msg) : msg;
msg = htmlEscape(msg);
msg = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(msg) : msg;
// Expose as variable, if demanded, else write to the page.

View File

@ -105,7 +105,7 @@ public class TransformTag extends HtmlEscapingAwareTag {
// Else, just do a toString.
result = this.value.toString();
}
result = isHtmlEscape() ? HtmlUtils.htmlEscape(result) : result;
result = htmlEscape(result);
if (this.var != null) {
pageContext.setAttribute(this.var, result, TagUtils.getScope(this.scope));
}

View File

@ -226,7 +226,7 @@ public class UrlTag extends HtmlEscapingAwareTag implements ParamAware {
}
// HTML and/or JavaScript escape, if demanded.
urlStr = isHtmlEscape() ? HtmlUtils.htmlEscape(urlStr) : urlStr;
urlStr = htmlEscape(urlStr);
urlStr = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(urlStr) : urlStr;
return urlStr;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -29,6 +29,7 @@ import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.util.WebUtils;
/**
* Tests for {@link MessageTag}.
@ -266,11 +267,31 @@ public class MessageTagTests extends AbstractTagTests {
}
};
tag.setPageContext(pc);
tag.setText("test & text");
tag.setText("test & text é");
tag.setHtmlEscape(true);
assertTrue("Correct doStartTag return value", tag.doStartTag() == Tag.EVAL_BODY_INCLUDE);
assertEquals("Correct doEndTag return value", Tag.EVAL_PAGE, tag.doEndTag());
assertEquals("Correct message", "test &amp; text", message.toString());
assertEquals("Correct message", "test &amp; text &eacute;", message.toString());
}
@SuppressWarnings("serial")
public void testMessageTagWithTextEncodingEscaped() throws JspException {
PageContext pc = createPageContext();
pc.getServletContext().setInitParameter(WebUtils.RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM, "true");
pc.getResponse().setCharacterEncoding("UTF-8");
final StringBuffer message = new StringBuffer();
MessageTag tag = new MessageTag() {
@Override
protected void writeMessage(String msg) {
message.append(msg);
}
};
tag.setPageContext(pc);
tag.setText("test <&> é");
tag.setHtmlEscape(true);
assertTrue("Correct doStartTag return value", tag.doStartTag() == Tag.EVAL_BODY_INCLUDE);
assertEquals("Correct doEndTag return value", Tag.EVAL_PAGE, tag.doEndTag());
assertEquals("Correct message", "test &lt;&amp;&gt; é", message.toString());
}
@SuppressWarnings("serial")