When writing a Java web application is very easy to inadvertently introduce security holes if you are primarily relying on security constraints specified in the web.xml. Security constraints specified in web.xml enforce access by examining the requested URL. This works fine as long as you aren’t programmatically dispatching pages within Java code.Hint: unless the web app has only static content and no forms then programmatic dispatch is happening.
Let’s consider a simple forgot password scenario for the admin portion of a web application:
- Admin goes to log into the restricted part of the web application and realizes that he/she has forgotten their password.
- Admin then clicks on the forgot password link which takes them to a page for resetting their password.
- After entering their username, the web application generates a reset email with a link and emails it to the address on file.
- User is redirected to the login page which displays a confirmation message that an email has been sent (which contains instructions on resetting the password).
A couple of notes about this scenario:
- All pages pertaining to administration are under /admin (admin/index.xhtml, admin/editAccount.xhtml, etc.)
- Form based security is in configured properly (using a realm).
- User must be a member of the admin group in order to access the pages under /admin.
Note: Although the code below will use JSF and CDI, this problem is not specific to these two technologies – this also applies to Struts and potentially any Java web application.
The web.xml has the following security setup:
[sourcecode language=”xml”]
<security-role>
<role-name>admin</role-name>
</security-role>
<security-constraint>
<display-name>Constraints-1</display-name>
<web-resource-collection>
<web-resource-name>admin-rsrc</web-resource-name>
<description/>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<description>authentication-required</description>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>ctjava</realm-name>
<form-login-config>
<form-login-page>/login.faces</form-login-page>
<form-error-page>/login_error.faces</form-error-page>
</form-login-config>
</login-config>
[/sourcecode]
This configuration snippets ensures that if someone requests http://ctjava.org/admin they will be routed to http://ctjava.org/login.faces if they have not already authenticated. This looks pretty clear-cut and you would assume that the pages under /admin are safe from the nefarious but this is not the case!
So let’s go back to our scenario. The admin lands on login.faces and realizes that they’ve forgotten their password so they click on the forgot password link. This takes them to reset.xhtml.
changePassword.xhtml:
[sourcecode language=”xhtml”]
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>${msgs.title}</title>
</h:head>
<h:body>
<div class="line"/>
<h:form>
Please enter the username for the account:
<label>Username:</label>
<h:inputText value="#{resetPasswordController.username}" size="25" maxlength="50"/>
<h:commandButton action="#{resetPasswordController.reset()}" value="Reset"/>
</h:form>
</h:body>
</html>
[/sourcecode]
This looks pretty simple, let’s take a look at the Java code which handles this request:
[sourcecode language=”java”]
@Model
public class ResetPasswordController {
// other methods
public String reset() {
accountManager.resetPassword(username);
facesContext.addMessage("none",new FacesMessage(FacesMessage.SEVERITY_INFO, "Password reset email sent.","Password reset email sent."));
return "admin";
}
}
[/sourcecode]
Then in our faces-config.xml we have the following:
[sourcecode language=”xml”]
<navigation-case>
<from-outcome>admin</from-outcome>
<to-view-id>/admin/index.faces</to-view-id>
</navigation-case>
[/sourcecode]
This all looks pretty simple. The user will enter their username into the form and then be routed to the admin login page (or at least that is the idea). By sending the user to /admin we are ‘expecting’ that the container will redisplay the login.faces page with the message that an email has been sent. Sounds good – right?
What is actually going to happen here is that the user will gain immediate access to the restricted pages under /admin. When we returned ‘admin’ from the action method, the page was retrieved internally. That is, we basically told the container to render /admin/index.faces (aka /admin/index.xhtml) directly. The container assumes that we know what we are doing and renders the page. This is REALLY bad!!
To correct this problem we of course have to fix the action but what about accidental code that does something like this? Although extensive testing will hopefully catch most of these how do we ensure that even if code like this exists, the page won’t get rendered?
When using Java EE we can annotate classes and methods with @RolesAllowed. It takes as a parameter the roles that are allowed to access the class/method. All admin code (beans) should be annotated with @RolesAllowed. The container will check the roles of the current user/session to see whether access is allowed. If not an exception will be thrown. Thus, even if a user is able to trigger navigation to a restricted page, the page will either fail to render because it attempts to invoke restricted methods or none of the actions on the page can be invoked because they invoke restricted actions.
This solution assumes that you are running in a Java EE container. This solution will not work in Tomcat. If you need security and you are using a web container like Tomcat you can either find a solution (like Spring) or switch to a Java EE container (open source free containers include GlassFish and JBoss). Of course you can roll your own solution but that probably doesn’t make any financial sense. One of the major features of Enterprise Java Beans is SECURITY.
Note: Java EE got much easier with Java EE 5 (Java EE 7 will be released in 2013). If you still have nightmares about EJB 1.x or 2.x try to forget the past. EJB 3.x is much easier to use and is really easy (POJOs and annotations!). With Glassfish 3+ and JBoss 5+, Java EE doesn’t require a gobs of RAM and a half-dozen classes to implement Hello World.
You should use return “admin?faces-redirect=true”;