ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Java Security

JSP Security for Limiting Access to Application-Internal URLs

06/27/2001

The applications that we deploy to the Web and our intranets are a significant source of security vulnerabilities. The technologies that we use to develop these applications make us more or less vulnerable to different types of attacks. For example, applications that are written in C++ tend to be more vulnerable to buffer-overrun attacks. Fortunately, Java-based Web development technologies (JSP, servlets, EJB, etc.) are less vulnerable to low-level programming errors and even provide some built-in security mechanisms.

While Java is a great language for building secure Web applications, careful attention to locking your application's interface is required to ensure success. In today's column, I'll cover a technique that I use to design and build simple JSP applications that provides some security benefits.

Application URLs Exposed

While many Web sites are still limited to simple collections of static content, more and more sites are deploying complex Web applications that interact with their users over a number of HTTP requests. Examples of these applications include personal information managers, online banking applications, and customer relationship services.

Building Web-based applications is challenging from a security perspective because HTTP is a stateless protocol, in which every browser-request/server-response is treated independently. There are a number of primary and secondary security issues that arise from this limitation. The one that I'll focus on is building applications that are distributed over several URLs.

A Web application that is accessible via several URLs is susceptible to URL-probing attacks. You may intend that your users access the individual application URLs in a way that makes sense for your application. However, some users (and most hackers) may not comply. Instead, they'll jump straight to the middle of your application and request URLs that are carefully calculated to circumvent your application's security features.

Comment on this articleWhat's your approach for security your Java Web applications using JSP or even other API such as EJB?
Post your comments

Also in Java Security:

Secure Your Sockets with JSSE

Java Plug-in 1.3 and RSA Signed Applets

In order to prevent users from probing my application's URLs for security weaknesses, I like to present a single application URL to my users. I model the application as a finite-state machine that changes from one secure state to another based upon interaction from the user.

The single application URL prevents a user/hacker from accessing the individual pages of the application. This doesn't mean that the application is written as a single page; it just means that the individual application pages are hidden from the user. At execution time, the application master page dynamically displays individual application pages to the user, processes user requests, determines the next application states, and transitions to these states. The application master page (which I'll just refer to as the application page) is automatically generated from an XML specification of the application.

Specifying the Application State Machine

Some of you may remember the finite-state machine from your computer science or math classes. If you don't, that's OK -- it is a very simple concept. A finite-state machine is a way of describing an application in terms of the states that the application may be in, the outputs that it displays, the inputs that it receives, and the ways in which it moves from state to state based on its current state and the inputs that it receives. Server-side Web applications are easily described in terms of a finite state machine. They start in a particular state, they receive a request from a user, they send a Web page to the user, and they move to a new state.

The following XML file presents a finite-state-machine design of an application that provides an on-line exam about basic Java security concepts. The application itself may be executed by clicking here.

<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE application SYSTEM "machina.dtd">

<application name="javasecurityexam" initial_state="welcome">

<state name="welcome"
output_action="welcome.jsp"
input_action="checkstart.jsp">
<transition next_state="welcome"/>
<transition condition="start" next_state="question"/>
<transition condition="help" type="call" next_state="help"/>
</state>

<state name="question"
output_action="question.jsp"
input_action="checkanswer.jsp">
<transition next_state="question"/>
<transition condition="correct" next_state="response"/>
<transition condition="incorrect" next_state="response"/>
<transition condition="help" type="call" next_state="help"/>
</state>

<state name="response"
output_action="response.jsp"
input_action="checkfinished.jsp">
<transition next_state="response"/>
<transition condition="nextQuestion" next_state="question"/>
<transition condition="finished" next_state="finished"/>
<transition condition="help" type="call" next_state="help"/>
</state>

<state name="finished"
output_action="finished.jsp">
<transition next_state="welcome"/>
</state>

<state name="help"
output_action="help.jsp"
input_action="checkreturn.jsp">
<transition next_state="help"/>
<transition condition="return" type="return"/>
</state>

</application>

The first two lines of the file are standard XML headers. If you are up to speed with XML Document Type Definitions, you can access the above document's DTD. Save the file as machina.dtd.

The application tag specifies that the application is named javasecurityexam and that its initial state is welcome. The remainder of the document consists of the definition of the welcome, question, response, finished, and help states. These states define all of the various states in which the javasecurityexam may reside during its interaction with the user.

The welcome state is the state in which the application resides when the user first starts the application. It displays the welcome.jsp page to the user and awaits input from the user. It executes checkstart.jsp to process the user's request and then transfers to exactly one of the states welcome, start, and help based on the input condition that is set by checkstart.jsp.

Let's examine each of the three transition tags of the welcome state. A transition tag has three attributes: condition, next_state, and type. The condition attribute specifies the condition under which the state transition is performed. If it is omitted, a default condition is assumed. The default condition is the condition that is used if no other conditions are specified. The first transition tag specifies that the default state transition should be back to the welcome state. This would occur if the user simply re-requested the same page. The second transition tag specifies that if the input condition set by checkstart.jsp is start then the application should transition to the question state. The third transition tag specifies that if the input condition set by checkstart.jsp is help, then the application should transition to the help state. Note that the type attribute is set. There are three types of state transitions: goto (the default), call, and return. Most transitions are of the type goto. The call transition type pushes the current state onto a stack before moving to the new state. The return type pops the state at the top of the stack and moves to that state. The specification of the help state contains an example of the return type.

When the application is in the question state, it displays a question to the user and then processes the user's response via checkanswer.jsp. It moves to the question, response, or help states, based on the condition that is set by checkanswer.jsp.

The response state is the state that the application is in when it displays a response to a user's answer. It displays the response via response.jsp, checks the response via checkfinished.jsp, and then transitions to the response, question, finished, or help states, based on the condition that is set by checkfinished.jsp.

The finished state is the state that the application is in when the user completes the exam. It displays the exam results via finished.jsp and then transitions back to the welcome state.

The help state is called by the application to display a help page (via help.jsp). The checkreturn.jsp page is used to determine whether the application should return from the help state or continue to display help.jsp.

From XML to JSP

The question that should be on your mind is, "OK, if I specify an application's XML file, how do I generate the application page?" That's another easy question to answer. I wrote an XSL transform that creates a JSP file from the XML file. It is available here. Save the file as machina.xsl.

If you're not familiar with XSLT, you can find more information at the W3C Web site. Don't worry about needing to learn the syntax of XSLT. All you have to do is be able to run an XSLT translator to transform your XML files to JSP files using machina.xsl. There are a number of XSLT translators that can be used to do the job. I like Instant Saxon and Xalan. Both tools come with excellent documentation.

For example, after downloading and installing Xalan, you can use the following command to translate exam.xml (the above document) to exam.jsp:

java org.apache.xalan.xslt.Process -in exam.xml -xsl machina.xsl -out exam.jsp

Make sure that exam.xml, machina.dtd, and machina.xsl are in the same directory.

Inside exam.jsp

The exam.jsp file that is generated from exam.xml (via machina.xsl) has the following content. This file is the master application file that users request to interact with the exam application.

<%@ page import="java.util.*" %>

<%!
// State machine data
static final String smName = "javasecurityexam";
static final String smInitialState = "welcome";

// State names
static final String[] states = {"welcome", "question", "response", "finished", "help"};

// Output actions corresponding to each state
static final String[] outputActions = {"welcome.jsp", "question.jsp", "response.jsp", "finished.jsp", "help.jsp"};

// Input actions corresponding to each state
static final String[] inputActions = {"checkstart.jsp", "checkanswer.jsp", "checkfinished.jsp", "", "checkreturn.jsp"};

// Input conditions that are defined for each state
static final String[][] inputConditions = {
{"default", "start", "help"},
{"default", "correct", "incorrect", "help"},
{"default", "nextQuestion", "finished", "help"},
{"default"},
{"default", "return"}
};

// Number of transitions defined for each state.
static final int[] numTransitions = {3, 4, 4, 1, 2};

// Cumulative number of transitions defined for all states before
// a particular state. Used to index outputActions, transitionType,
// and nextState.
static final int[] cumulativeTransitions = {0, 3, 7, 11, 12};

// State transition type (goto, call, or return) associated with a state transition.
static final String[] transitionTypes = {"goto", "goto", "call", "goto", "goto", "goto", "call", "goto", "goto", "goto", "call", "goto", "goto", "return"};

// Next state associated with a state transition.
static final String[] nextStates = {"welcome", "question", "help", "question", "response", "response", "help", "response", "question", "finished", "help", "welcome", "help", ""};

// Returns the index of state in states.
static final int getStateIndex(String state) {
    for(int i=0; i<states.length; ++i) {
        if(states[i].equals(state)) return i;
    }
    return -1;
}

// Returns the index of (state, condition) in transitionType and nextStates.
static int getConditionIndex(int stateIndex, String condition) {
    if(stateIndex == -1) return -1;
    for(int i=0; i<inputConditions[stateIndex].length; ++i) {
        if(inputConditions[stateIndex][i].equals(condition)) {
            return (cumulativeTransitions[stateIndex] + i);
        }
    }
    return -1;
}
%>

<%
// Get the instance's ID
String smID = request.getParameter("smID");
// Define state and stack variables
String state = "";
Stack stack = null;
// Define sm and smInstance Hashtable objects
Hashtable sm = null;
Hashtable smInstance = null;
boolean newInstance = false;
// Check for start/restart
synchronized(session) {
    // sm links to all sm instances for this session
    sm = (Hashtable) session.getAttribute(smName);
    // First time sm is used in this session?
    if(sm == null) {
        // Create a new sm
        sm = new Hashtable();
        // Set the number of instances to 0
        sm.put("instanceCount", "0");
    }
    // Obtain access to an smInstance
    synchronized(sm) {
        // Start a new instance?
        if(smID == null || sm.get(smID) == null) {
            newInstance = true;
            String instanceCountString = (String) sm.get("instanceCount");
            int instanceCount = Integer.decode(instanceCountString).intValue();
            // Create an instance of this sm
            smInstance = new Hashtable();
            // Give it a state and a stack
            smInstance.put("state", smInitialState);
            smInstance.put("stack", new Stack());
            // Link sm to the new instance
            sm.put("instanceCount", ""+(instanceCount+1));
            sm.put("" + instanceCount, smInstance);
            // Update sm in the session
            session.setAttribute(smName, sm);
            smID = instanceCountString;
        }
        smInstance = (Hashtable) sm.get(smID);
        state = (String) smInstance.get("state");
        stack = (Stack) smInstance.get("stack");
    }
}
// Notify called pages of sm's name and ID (in case of new smID)
request.setAttribute("smName", smName);
request.setAttribute("smID", smID);
// Perform input actions
int stateIndex = getStateIndex(state);
// If new instance of application then just display page
if(newInstance) {
    response.sendRedirect(request.getRequestURI()+"?smID="+smID);
    return;
}
// Otherwise perform input action, transition, and display new page
String inputAction = inputActions[stateIndex];
if(!inputAction.equals("")) pageContext.include(inputAction);
// Get condition from request
String condition = (String) request.getAttribute("smCondition");
// If condition is not defined, try the default
if(condition == null) condition = "default";
int conditionIndex = getConditionIndex(stateIndex, condition);
// If condition is not defined and it is not the default, try the default condition
if(conditionIndex == -1 && !condition.equals("default")) {
    condition = "default";
    conditionIndex = getConditionIndex(stateIndex, condition);
}
// If no valid condition, simply redisplay the current page
if(conditionIndex == -1) {
    pageContext.include(outputActions[stateIndex]);
    return;
}
// Change state
String transitionType = transitionTypes[conditionIndex];
String nextState = nextStates[conditionIndex];
if(transitionType.equals("call")) stack.push(state);
else if(transitionType.equals("return")) nextState = (String) stack.pop();
// Save state and stack in smInstance
synchronized(session) {
    sm = (Hashtable) session.getAttribute(smName);
    synchronized(sm) {
        smInstance = (Hashtable) sm.get(smID);
        synchronized(smInstance) {
            smInstance.put("state", nextState);
            smInstance.put("stack", stack);
        }
        sm.put(smID, smInstance);
    }
    session.setAttribute(smName, sm);
}
// Display output
pageContext.include(outputActions[getStateIndex(nextState)]);
%>

Integrating Application Files

One thing that you'll appreciate is that you'll never have to edit the master application file (e.g., exam.jsp). However, you should make a pass through it to understand how it works.

The exam.jsp file begins by defining a number of field variables:

In addition, the getStateIndex() and getConditionIndex() methods are defined. The getStateIndex() method returns the index of a state in states. The getConditionIndex() method returns the index of a state and condition in transitionType and nextStates.

The applications that are generated via machina.xsl provide the capability to execute multiple instances of an application within a single user session. This enables the user to open more than one browser window and execute the application independently in each window. For example, if your application is an online XML editor, the user will be able to edit different XML documents in separate windows. The individual application instance is tracked via an smID parameter that accompanies each user request. You are responsible for propagating this parameter in your application's URLs. Don't worry -- I'll provide you with several examples of how this is accomplished.

Besides propagating the smID parameter, your input actions are required to set input conditions. This is accomplished by setting the current request's smCondition attribute. Again, you'll see more of this in the exam example.

Back to the Application Code

The main part of the page's processing begins by checking for the smID parameter, and retrieving the application instance's state and stack. A new application instance is created if the smID parameter is not supplied.

The application notifies the output and input action pages of its name and smID by setting the smName and smID request attributes.

The state's input action is executed via a <jsp:include> action. The resulting smCondition attribute is retrieved from the request object. A state transition is then performed, based on the value of the smCondition value.

The current values of state and stack are saved in the user's session, and the output action associated with the new state is displayed.

The Exam Application Files

From the outputActions array, you can see that the application makes use of the following pages to display output to the user:

These and all other source JSP pages of the application can be downloaded here.

What prevents the user from directly requesting the URLs of these pages? At the top of every internal page of the application I insert the following <jsp:include> action:

<jsp:include page="verifyinclude.jsp" flush="true" />

The source of verifyinclude.jsp is as follows:

<%
String smName = (String) request.getAttribute("smName");
if(smName == null)
  throw new Exception("Page not called from within application.");
%>

When a user requests an internal application page, an exception is thrown and the user receives a nasty error. You may want to handle this error with a JSP error-handling page.

As previously mentioned, each output action is responsible for propagating the smID request parameter in the application URLs. This is fairly straightforward. First you retrieve the value of smID from the current request and then add it to any application links that are displayed. In welcome.jsp, the href attribute of the Start link is calculated as follows:

String start = request.getRequestURI() + "?smID=" +
 request.getParameter("smID") + "&start=true";

In question.jsp, the smID parameter is stored in a hidden form field:

<form name="question" action="<%= request.getRequestURI()%>" method="get">
<% for(int i=1; i<questions[currentQuestion].length; ++i) { %>
<p class="answer"><%= i %>. <input type="checkbox" name='a<%= i %>'>
<%= questions[currentQuestion][i] %></p>
<% } %>
<input type='hidden' name='smID' value='<%= request.getAttribute("smID")%>'>
<p class="answer"><input type="submit" name="submit" value="Submit"></p>
</form>

You can use whatever technique you like, as long as smID shows up as a parameter of the request object.

Working with Input Actions

Input actions are simple JSPs that process request parameters and set the smCondition attribute of the request object. From the inputActions array, you can see that the application has the following input actions:

The checkstart.jsp page is fairly typical of what you'll put in your input actions:

<%
if(request.getParameter("help") != null)
 request.setAttribute("smCondition", "help");
else if(request.getParameter("start") != null)
 request.setAttribute("smCondition", "start");
%>

It checks the values of the request parameters and then reflects them in the smCondition attribute. Of course, more complicated checks may also be made.

More General Application Frameworks

The JSP application framework that I presented in this column is designed to be simple, secure, and verifiable. (For some customers, I actually verify the design of some security-sensitive Web applications.) However, there are other more general, powerful, and comprehensive application frameworks that you can use. The Struts framework of the Apache-Jakarta project is an example of the latter. Struts goes beyond a simple state machine to provide extensive support for the Model-View-Controller paradigm.

Jamie Jaworski is a Java security expert and author, focused on cryptography and such.


Read more Java Security columns.

Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.