/**
* OWASP Enterprise Security API (ESAPI)
*
* This file is part of the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. For details, please see
* http://www.owasp.org/esapi.
*
* Copyright (c) 2007 - The OWASP Foundation
*
* The ESAPI is published by OWASP under the LGPL. You should read and accept the
* LICENSE before you use, modify, and/or redistribute this software.
*
* @author Jeff Williams Aspect Security
* @created 2007
*/
package org.owasp.esapi;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.owasp.esapi.errors.AccessControlException;
import org.owasp.esapi.errors.AuthenticationException;
import org.owasp.esapi.errors.EncodingException;
import org.owasp.esapi.errors.EncryptionException;
import org.owasp.esapi.errors.IntrusionException;
import org.owasp.esapi.errors.ValidationException;
import org.owasp.esapi.errors.ValidationUploadException;
/**
* Reference implementation of the IHTTPUtilities interface. This implementation
* uses the Apache Commons FileUploader library, which in turn uses the Apache
* Commons IO library.
*
* To simplify the interface, this class uses the current request and response that
* are tracked by ThreadLocal variables in the Authenticator. This means that you
* must have called ESAPI.authenticator().setCurrentHTTP(null, response) before
* calling these methods. This is done automatically by the Authenticator.login() method.
*
* @author Jeff Williams (jeff.williams .at. aspectsecurity.com) Aspect Security
* @since June 1, 2007
* @see org.owasp.esapi.interfaces.IHTTPUtilities
*/
public class HTTPUtilities implements org.owasp.esapi.interfaces.IHTTPUtilities {
/** The logger. */
private static final Logger logger = Logger.getLogger("ESAPI", "HTTPUtilities");
/** The max bytes. */
int maxBytes = ESAPI.securityConfiguration().getAllowedFileUploadSize();
public HTTPUtilities() {
}
// FIXME: Enhance - consider adding addQueryChecksum(String href) that would just verify that none of the parameters in the querystring have changed. Could do the same for forms.
// FIXME: Enhance - also verifyQueryChecksum()
// FIXME: need to make this easier to add to forms.
/**
* @see org.owasp.esapi.interfaces.IHTTPUtilities#addCSRFToken(java.lang.String)
*/
public String addCSRFToken(String href) {
User user = ESAPI.authenticator().getCurrentUser();
// FIXME: AAA getCurrentUser should never return null
if (user.isAnonymous() || user == null) {
return href;
}
if ( ( href.indexOf( '?') != -1 ) || ( href.indexOf( '&' ) != -1 ) ) {
return href + "&" + user.getCSRFToken();
} else {
return href + "?" + user.getCSRFToken();
}
}
/**
* Adds a cookie to the HttpServletResponse that uses Secure and HttpOnly
* flags. This implementation does not use the addCookie method because
* it does not support HttpOnly, so it just creates a cookie header manually.
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#safeAddCookie(java.lang.String,
* java.lang.String, java.util.Date, java.lang.String,
* java.lang.String, javax.servlet.http.HttpServletResponse)
*/
public void safeAddCookie(String name, String value, int maxAge, String domain, String path) {
HttpServletResponse response = getCurrentResponse();
try {
String cookieName = ESAPI.validator().getValidInput( "safeAddCookie", name, "HTTPCookieName", 50, false);
String cookieValue = ESAPI.validator().getValidInput( "safeAddCookie", value, "HTTPCookieValue", 5000, false);
// FIXME: AAA need to validate domain and path! Otherwise response splitting etc.. Can use Cookie object?
// create the special cookie header
// Set-Cookie:=[; =][; expires=][;
// domain=][; path=][; secure][;HttpOnly
// FIXME: AAA test if setting a separate set-cookie header for each cookie works!
String header = cookieName + "=" + cookieValue;
if ( maxAge != -1 ) header += "; Max-Age=" + maxAge;
if ( domain != null ) header += "; Domain=" + domain;
if ( path != null ) header += "; Path=" + path;
header += "; Secure; HttpOnly";
response.addHeader("Set-Cookie", header);
} catch( ValidationException e ) {
logger.logWarning(Logger.SECURITY, "Attempt to set invalid cookie denied", e);
}
}
/*
* Adds a header to an HttpServletResponse after checking for special
* characters (such as CRLF injection) that could enable attacks like
* response splitting and other header-based attacks that nobody has thought
* of yet.
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#safeAddHeader(java.lang.String,
* java.lang.String, java.lang.String,
* javax.servlet.http.HttpServletResponse)
*/
public void safeAddHeader(String name, String value) {
// FIXME: this isn't a drop-in replacement -- need to make safe behavior with a log message
HttpServletResponse response = getCurrentResponse();
try {
String headerName = ESAPI.validator().getValidInput( "safeAddHeader", name, "HTTPHeaderName", 50, false);
String headerValue = ESAPI.validator().getValidInput( "safeAddHeader", value, "HTTPHeaderValue", 500, false);
response.addHeader(headerName, headerValue);
} catch( ValidationException e ) {
logger.logWarning(Logger.SECURITY, "Attempt to set invalid header denied", e);
}
}
// FIXME: make configurable
public void safeSendError(int sc) throws IOException {
HttpServletResponse response = getCurrentResponse();
response.sendError(response.SC_OK, getHttpMessage(sc) );
}
// FIXME: make configurable
public void safeSendError(int sc, String msg) throws IOException {
HttpServletResponse response = getCurrentResponse();
// FIXME: safe msg
response.sendError(response.SC_OK, msg );
}
/* returns a text message for the http response code */
private String getHttpMessage( int sc ) {
// FIXME: implement
return "HTTP error code: " + sc;
}
public void safeSetDateHeader( String name, long date ) {
HttpServletResponse response = getCurrentResponse();
try {
String safeName = ESAPI.validator().getValidInput("safeSetDateHeader", name, "HTTPHeaderName", 20, false);
response.setDateHeader(safeName, date);
} catch (ValidationException e) {
logger.logWarning(Logger.SECURITY, "Attempt to set invalid date header name denied", e);
}
}
public void safeSetIntHeader( String name, int value ) {
HttpServletResponse response = getCurrentResponse();
try {
String safeName = ESAPI.validator().getValidInput("safeSetDateHeader", name, "HTTPHeaderName", 20, false);
response.setIntHeader(safeName, value);
} catch (ValidationException e) {
logger.logWarning(Logger.SECURITY, "Attempt to set invalid int header name denied", e);
}
}
public void safeSetCharacterEncodingInResponse( String charset ) {
HttpServletResponse response = getCurrentResponse();
response.setCharacterEncoding(charset);
}
public void safeAddCookie( Cookie cookie ) {
HttpServletResponse response = getCurrentResponse();
response.addCookie(cookie);
}
public void safeSetLocale( Locale loc ) {
HttpServletResponse response = getCurrentResponse();
response.setLocale(loc);
}
public void safeSetStatus( int sc ) {
HttpServletResponse response = getCurrentResponse();
response.setStatus(sc);
}
public void safeSetStatus( int sc, String sm ) {
HttpServletResponse response = getCurrentResponse();
// FIXME: safe message
response.setStatus(response.SC_OK, sm);
}
public void safeSetCharacterEncodingInRequest( String env ) throws UnsupportedEncodingException {
getCurrentRequest().setCharacterEncoding(env);
}
/*
* Sets a header in an HttpServletResponse after checking for special
* characters (such as CRLF injection) that could enable attacks like
* response splitting and other header-based attacks that nobody has thought
* of yet.
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#safeAddHeader(java.lang.String,
* java.lang.String, java.lang.String,
* javax.servlet.http.HttpServletResponse)
*/
public void safeSetHeader(String name, String value) throws ValidationException {
HttpServletResponse response = getCurrentResponse();
try {
String safeName = ESAPI.validator().getValidInput("setSafeHeader", name, "HTTPHeaderName", 20, false);
String safeValue = ESAPI.validator().getValidInput("setSafeHeader", value, "HTTPHeaderValue", 500, false);
response.setHeader(safeName, safeValue);
} catch (ValidationException e) {
logger.logWarning(Logger.SECURITY, "Attempt to set invalid header denied", e);
}
}
//FIXME: AAA add these to the interface
/**
* Return exactly what was sent to prevent URL rewriting. URL rewriting is intended to be a session management
* scheme that doesn't require cookies, but exposes the sessionid in many places, including the URL bar,
* favorites, HTML files in cache, logs, and cut-and-paste links. For these reasons, session rewriting is
* more dangerous than the evil cookies it was intended to replace.
*
* @param url
* @return
*/
public String safeEncodeURL( String url ) {
return url;
}
/**
* Return exactly what was sent to prevent URL rewriting. URL rewriting is intended to be a session management
* scheme that doesn't require cookies, but exposes the sessionid in many places, including the URL bar,
* favorites, HTML files in cache, logs, and cut-and-paste links. For these reasons, session rewriting is
* more dangerous than the evil cookies it was intended to replace.
*
* @param url
* @return
*/
public String safeEncodeRedirectURL( String url ) {
return url;
}
/*
* (non-Javadoc)
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#changeSessionIdentifier(javax.servlet.http.HttpServletRequest)
*/
public HttpSession changeSessionIdentifier() throws AuthenticationException {
HttpServletRequest request = getCurrentRequest();
Map temp = new HashMap();
HttpSession session = request.getSession( false );
// make a copy of the session content
if ( session != null ) {
Enumeration e = session.getAttributeNames();
while (e != null && e.hasMoreElements()) {
String name = (String) e.nextElement();
Object value = session.getAttribute(name);
temp.put(name, value);
}
session.invalidate();
}
HttpSession newSession = request.getSession(true);
// copy back the session content
Iterator i = temp.entrySet().iterator();
while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next();
newSession.setAttribute((String) entry.getKey(), entry.getValue());
}
return newSession;
}
// FIXME: ENHANCE - add configuration for entry pages that don't require a token
/*
* This implementation uses the parameter name to store the token.
* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#verifyCSRFToken()
*/
public void verifyCSRFToken() throws IntrusionException {
HttpServletRequest request = getCurrentRequest();
User user = ESAPI.authenticator().getCurrentUser();
if( request.getAttribute(user.getCSRFToken()) != null ) return;
if (request.getParameter(user.getCSRFToken()) == null) {
throw new IntrusionException("Authentication failed", "Possibly forged HTTP request without proper CSRF token detected");
}
}
/*
* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#decryptHiddenField(java.lang.String)
*/
public String decryptHiddenField(String encrypted) {
try {
return ESAPI.encryptor().decrypt(encrypted);
} catch( EncryptionException e ) {
throw new IntrusionException("Invalid request","Tampering detected. Hidden field data did not decrypt properly.", e);
}
}
/*
* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#decryptQuueryString(java.lang.String)
*/
public Map decryptQueryString(String encrypted) throws EncryptionException {
// FIXME: AAA needs test cases
String plaintext = ESAPI.encryptor().decrypt(encrypted);
return queryToMap(plaintext);
}
/**
* @throws EncryptionException
* @see org.owasp.esapi.interfaces.IHTTPUtilities#decryptStateFromCookie()
*/
public Map decryptStateFromCookie() throws EncryptionException {
// FIXME: consider getEncryptedCookieValue( String name )
HttpServletRequest request = getCurrentRequest();
Cookie[] cookies = request.getCookies();
Cookie c = null;
for ( int i = 0; i < cookies.length; i++ ) {
if ( cookies[i].getName().equals( "state" ) ) {
c = cookies[i];
}
}
String encrypted = c.getValue();
String plaintext = ESAPI.encryptor().decrypt(encrypted);
return queryToMap( plaintext );
}
/*
* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#encryptHiddenField(java.lang.String)
*/
public String encryptHiddenField(String value) throws EncryptionException {
// FIXME: this needs better support
// like cookie with name-value pairs
// and an easy way to decrypt to a hashmap
return ESAPI.encryptor().encrypt(value);
}
/*
* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#encryptQueryString(java.lang.String)
*/
public String encryptQueryString(String query) throws EncryptionException {
// FIXME: this needs better support
// like cookie with name-value pairs
// and an easy way to decrypt to a hashmap
return ESAPI.encryptor().encrypt( query );
}
/**
* @throws EncryptionException
* @see org.owasp.esapi.interfaces.IHTTPUtilities#encryptStateInCookie(java.util.Map)
*/
public void encryptStateInCookie(Map cleartext) throws EncryptionException {
StringBuffer sb = new StringBuffer();
Iterator i = cleartext.entrySet().iterator();
while ( i.hasNext() ) {
try {
Map.Entry entry = (Map.Entry)i.next();
String name = ESAPI.encoder().encodeForURL( entry.getKey().toString() );
String value = ESAPI.encoder().encodeForURL( entry.getValue().toString() );
sb.append( name + "=" + value );
if ( i.hasNext() ) sb.append( "&" );
} catch( EncodingException e ) {
logger.logError(Logger.SECURITY, "Problem encrypting state in cookie - skipping entry", e );
}
}
// FIXME: AAA - add a check to see if cookie length will exceed 2K limit
String encrypted = ESAPI.encryptor().encrypt(sb.toString());
this.safeAddCookie("state", encrypted, -1, null, null );
}
/**
* Uses the Apache Commons FileUploader to parse the multipart HTTP request
* and extract any files therein. Note that the progress of any uploads is
* put into a session attribute, where it can be retrieved with a simple
* JSP.
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#safeGetFileUploads(javax.servlet.http.HttpServletRequest,
* java.io.File, java.io.File, int)
* @return list of File objects for new files in final directory
*/
public List getSafeFileUploads(File tempDir, File finalDir) throws ValidationException {
if ( !tempDir.exists() ) tempDir.mkdirs();
if ( !finalDir.exists() ) finalDir.mkdirs();
List newFiles = new ArrayList();
HttpServletRequest request = getCurrentRequest();
try {
final HttpSession session = request.getSession();
if (!ServletFileUpload.isMultipartContent(request)) {
throw new ValidationUploadException("Upload failed", "Not a multipart request");
}
// this factory will store ALL files in the temp directory,
// regardless of size
DiskFileItemFactory factory = new DiskFileItemFactory(0, tempDir);
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setSizeMax(maxBytes);
// Create a progress listener
ProgressListener progressListener = new ProgressListener() {
private long megaBytes = -1;
private long progress = 0;
public void update(long pBytesRead, long pContentLength, int pItems) {
if (pItems == 0)
return;
long mBytes = pBytesRead / 1000000;
if (megaBytes == mBytes)
return;
megaBytes = mBytes;
progress = (long) (((double) pBytesRead / (double) pContentLength) * 100);
session.setAttribute("progress", Long.toString(progress));
// logger.logSuccess(Logger.SECURITY, " Item " + pItems + " (" + progress + "% of " + pContentLength + " bytes]");
}
};
upload.setProgressListener(progressListener);
List items = upload.parseRequest(request);
Iterator i = items.iterator();
while (i.hasNext()) {
FileItem item = (FileItem) i.next();
if (!item.isFormField() && item.getName() != null && !(item.getName().equals("")) ) {
String[] fparts = item.getName().split("[\\/\\\\]");
String filename = fparts[fparts.length - 1];
if (!ESAPI.validator().isValidFileName("upload", filename, false)) {
throw new ValidationUploadException("Upload only simple filenames with the following extensions " + ESAPI.securityConfiguration().getAllowedFileExtensions(), "Upload failed isValidFileName check");
}
logger.logCritical(Logger.SECURITY, "File upload requested: " + filename);
File f = new File(finalDir, filename);
if (f.exists()) {
String[] parts = filename.split("\\/.");
String extension = "";
if (parts.length > 1) {
extension = parts[parts.length - 1];
}
String filenm = filename.substring(0, filename.length() - extension.length());
f = File.createTempFile(filenm, "." + extension, finalDir);
}
item.write(f);
newFiles.add( f );
// delete temporary file
item.delete();
logger.logCritical(Logger.SECURITY, "File successfully uploaded: " + f);
session.setAttribute("progress", Long.toString(0));
}
}
} catch (Exception e) {
if (e instanceof ValidationUploadException)
throw (ValidationException) e;
throw new ValidationUploadException("Upload failure", "Problem during upload:" + e.getMessage(), e);
}
return newFiles;
}
/**
* Returns true if the request was transmitted over an SSL enabled
* connection. This implementation ignores the built-in isSecure() method
* and uses the URL to determine if the request was transmitted over SSL.
*/
public boolean isSecureChannel() {
HttpServletRequest request = getCurrentRequest();
return (request.getRequestURL().charAt(4) == 's');
}
/*
* (non-Javadoc)
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#killAllCookies(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
public void killAllCookies() {
HttpServletRequest request = getCurrentRequest();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
Cookie cookie = cookies[i];
killCookie(cookie.getName());
}
}
}
/*
* (non-Javadoc)
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#killCookie(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
public void killCookie(String name) {
HttpServletRequest request = getCurrentRequest();
HttpServletResponse response = getCurrentResponse();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
Cookie cookie = cookies[i];
if (cookie.getName().equals(name)) {
String path = request.getContextPath();
String header = name + "=deleted; Max-Age=0; Path=" + path;
response.addHeader("Set-Cookie", header);
}
}
}
}
private Map queryToMap(String query) {
TreeMap map = new TreeMap();
String[] parts = query.split("&");
for ( int j = 0; j < parts.length; j++ ) {
try {
String[] nvpair = parts[j].split("=");
String name = ESAPI.encoder().decodeFromURL(nvpair[0]);
String value = ESAPI.encoder().decodeFromURL(nvpair[1]);
map.put( name, value);
} catch( EncodingException e ) {
// skip and continue
}
}
return map;
}
/*
* (non-Javadoc)
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#safeSendForward(java.lang.String)
*/
public void safeSendForward(String context, String location) throws AccessControlException,ServletException,IOException {
// FIXME: should this be configurable? What is a good forward policy?
// I think not allowing forwards to public URLs is good, as it bypasses many access controls
HttpServletRequest request = getCurrentRequest();
HttpServletResponse response = getCurrentResponse();
if (!location.startsWith("WEB-INF")) {
throw new AccessControlException("Forward failed", "Bad forward location: " + location);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(location);
dispatcher.forward(request, response);
}
/*
* (non-Javadoc)
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#safeSendRedirect(java.lang.String)
*/
public void safeSendRedirect(String context, String location) throws IOException {
HttpServletResponse response = getCurrentResponse();
if (!ESAPI.validator().isValidRedirectLocation(context, location, false)) {
logger.logCritical(Logger.SECURITY, "Bad redirect location: " + location );
throw new IOException("Redirect failed");
}
response.sendRedirect(location);
}
/**
* Set the character encoding on every HttpServletResponse in order to limit
* the ways in which the input data can be represented. This prevents
* malicious users from using encoding and multi-byte escape sequences to
* bypass input validation routines. The default is text/html; charset=UTF-8
* character encoding, which is the default in early versions of HTML and
* HTTP. See RFC 2047 (http://ds.internic.net/rfc/rfc2045.txt) for more
* information about character encoding and MIME.
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#safeSetContentType(java.lang.String)
*/
public void safeSetContentType() {
HttpServletResponse response = getCurrentResponse();
response.setContentType(((SecurityConfiguration)ESAPI.securityConfiguration()).getResponseContentType());
}
/**
* Set headers to protect sensitive information against being cached in the
* browser.
*
* @see org.owasp.esapi.interfaces.IHTTPUtilities#setNoCacheHeaders(javax.servlet.http.HttpServletResponse)
*/
public void setNoCacheHeaders() {
HttpServletResponse response = getCurrentResponse();
// HTTP 1.1
response.setHeader("Cache-Control", "no-store");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Cache-Control", "must-revalidate");
// HTTP 1.0
response.setHeader("Pragma","no-cache");
response.setDateHeader("Expires", -1);
}
/*
* The currentRequest ThreadLocal variable is used to make the currentRequest available to any call in any part of an
* application. This enables API's for actions that require the request to be much simpler. For example, the logout()
* method in the Authenticator class requires the currentRequest to get the session in order to invalidate it.
*/
private ThreadLocalRequest currentRequest = new ThreadLocalRequest();
private class ThreadLocalRequest extends InheritableThreadLocal {
public Object initialValue() {
return null;
}
public HttpServletRequest getRequest() {
return (HttpServletRequest)super.get();
}
public void setRequest(HttpServletRequest newRequest) {
super.set(newRequest);
}
};
/*
* The currentResponse ThreadLocal variable is used to make the currentResponse available to any call in any part of an
* application. This enables API's for actions that require the response to be much simpler. For example, the logout()
* method in the Authenticator class requires the currentResponse to kill the JSESSIONID cookie.
*/
private ThreadLocalResponse currentResponse = new ThreadLocalResponse();
private class ThreadLocalResponse extends InheritableThreadLocal {
public Object initialValue() {
return null;
}
public HttpServletResponse getResponse() {
return (HttpServletResponse)super.get();
}
public void setResponse(HttpServletResponse newResponse) {
super.set(newResponse);
}
};
/* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#getCurrentRequest()
*/
public HttpServletRequest getCurrentRequest() {
HttpServletRequest request = (HttpServletRequest)currentRequest.get();
if ( request == null ) throw new NullPointerException( "Cannot use current request until it is set, typically via login" );
return request;
}
/* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#getCurrentResponse()
*/
public HttpServletResponse getCurrentResponse() {
HttpServletResponse response = (HttpServletResponse)currentResponse.get();
if ( response == null ) throw new NullPointerException( "Cannot use current response until it is set, typically via login" );
return response;
}
/* (non-Javadoc)
* @see org.owasp.esapi.interfaces.IHTTPUtilities#setCurrentHttp(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public void setCurrentHTTP(HttpServletRequest request, HttpServletResponse response) {
currentRequest.set(request);
currentResponse.set(response);
}
}