ELResolver Escapes JSP EL Values To Prevent Cross-Site Scripting
Cross-site scripting is a computer security vulnerability enabling an attacker to inject malicious code into a Web page that will be executed by the Web browser when other users view the page. If your Web application accepts data from the user and then outputs that data unaltered in HTML, then it is vulnerable because user-controlled data might contain executable code.
Since JSP 2.0, EL expressions can appear in the template text:
<h1>Hello, ${user.name}</h1>
Unfortunately, the JSP container does not escape expression values, so if the
expression contains user-controlled data, then cross-site scripting is
possible. JSTL provides a couple of ways to sanitize the output. The c:out
tag escapes XML characters by default:
<c:out value="${user.name}"/>
Alternatively, the EL function fn:escapeXml
also escapes XML characters:
${fn:escapeXml(user.name)}
The default option should be the safe option. That’s a sensible engineering
principle. If EL values are escaped by default, then you’re protected from
coders who forget to wrap expressions in c:out
or fn:escapeXml
.
Starting with JSP 2.1, a Web application can register a custom ELResolver. I’m going to present a custom ELResolver that escapes EL values, allowing you to use EL in JSPs while preventing cross-site scripting.
A
custom servlet context listener
registers the custom ELResolver when the application starts. To use
it, define a listener in the web.xml
file:
<listener>
<listener-class>com.github.pukkaone.jsp.EscapeXmlELResolverListener</listener-class>
</listener>
Disable Escaping
When you register this custom ELResolver, all EL values will be escaped by
default. If you want a JSP to programmatically output HTML, you can resort to
a
JSP scriptlet
or
JSP expression,
unless the application configured scripting-invalid
to true
.
Another way uses a custom tag to surround JSP code in which EL values should not be escaped:
<%@ taglib prefix="enhance" uri="http://pukkaone.github.com/jsp" %>
<enhance:out escapeXml="false">
I hope this expression returns safe HTML: ${user.name}
</enhance:out>
The escapeXml
attribute is true
by default. You must explicitly set it to
false
in the tag to disable escaping.
Details
The servlet context listener’s contextInitialized
method calls the
JspApplicationContext.addELResolver
method to register the custom ELResolver.
public void contextInitialized(ServletContextEvent event) {
JspFactory.getDefaultFactory()
.getJspApplicationContext(event.getServletContext())
.addELResolver(new EscapeXmlELResolver());
}
The addELResolver
method inserts the custom ELResolver into a chain of
standard resolvers. When evaluating an expression, the JSP container consults
the chain of resolvers in the following order, stopping at the first resolver
to succeed:
-
ImplicitObjectELResolver
-
ELResolvers registered by the addELResolver method.
-
MapELResolver
-
ListELResolver
-
ArrayELResolver
-
BeanELResolver
-
ScopedAttributeELResolver
This presents a slight problem because the custom ELResolver wants to escape the value that would have resulted from consulting the chain. When asked for a value, the custom ELResolver invokes the chain of resolvers. The custom ELResolver is itself in the chain of resolvers, so before invoking the chain, it sets a flag telling itself to do nothing when its turn in the chain comes around.
private boolean gettingValue;
@Override
public Object getValue(ELContext context, Object base, Object property)
throws NullPointerException, PropertyNotFoundException, ELException
{
if (gettingValue) {
return null;
}
gettingValue = true;
Object value = context.getELResolver().getValue(
context, base, property);
gettingValue = false;
if (value instanceof String) {
value = EscapeXml.escape((String) value);
}
return value;
}
There’s a resolver in the chain before the custom ELResolver. This resolver, ImplicitObjectELResolver, will be invoked twice. First, before reaching the custom ELResolver, and again when the custom ELResolver invokes the chain. Multiple invocations of ImplicitObjectELResolver is harmless because ImplicitObjectELResolver had to fail in order for the custom ELResolver to be invoked. When the custom ELResolver invokes the chain, the ImplicitObjectELResolver will fail again.
A resolver indicates success by setting the propertyResolved
property of the
ELContext
to true
. When consulting the chain, one of the resolvers very
likely set this property to true
, so no other resolvers are invoked after
returning from the custom ELResolver.