diff -r 000000000000 -r 3060119b1292 classes/hirondelle/web4j/security/ApplicationFirewallImpl.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/classes/hirondelle/web4j/security/ApplicationFirewallImpl.java Wed Dec 04 17:00:31 2013 +0100
@@ -0,0 +1,379 @@
+package hirondelle.web4j.security;
+
+import static hirondelle.web4j.util.Consts.FAILS;
+import hirondelle.web4j.BuildImpl;
+import hirondelle.web4j.action.Action;
+import hirondelle.web4j.action.Operation;
+import hirondelle.web4j.model.BadRequestException;
+import hirondelle.web4j.model.Id;
+import hirondelle.web4j.readconfig.Config;
+import hirondelle.web4j.readconfig.ConfigReader;
+import hirondelle.web4j.request.RequestParameter;
+import hirondelle.web4j.request.RequestParser;
+import hirondelle.web4j.util.Util;
+
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ Default implementation of {@link ApplicationFirewall}.
+
+
Upon startup, this class will inspect all {@link Action}s in the application.
+ All public static final {@link hirondelle.web4j.request.RequestParameter} fields accessible
+ to each {@link Action} will be collected, and treated here as the set of acceptable
+ {@link RequestParameter}s for each {@link Action} class. Thus, when this class is used to implement
+ {@link ApplicationFirewall}, each {@link Action} must declare all expected request
+ parameters as a public static final {@link RequestParameter} field, in order to pass hard validation.
+
+
File Upload Forms
+ If a POSTed request includes one or more file upload controls, then the underlying HTTP request has
+ a completely different structure from a regular request having no file upload controls.
+ Unfortunately, the Servlet API has very poor support for forms that include a file upload control: only the raw underlying request is available, in an unparsed form.
+ For such forms, POSTed data is not available in the usual way, and by default request.getParameter(String) will return null -
+ not only for the file upload control, but for all controls in the form.
+
+ An elegant way around this problem involves wrapping the request,
+ using {@link javax.servlet.http.HttpServletRequestWrapper}, such that POSTed data is parsed and made
+ available through the usual request methods.
+ If such a wrapper is used, then file upload forms can be handled in much the same way as any other form.
+
+
To indicate to this class if such a wrapper is being used for file upload requests, use the FullyValidateFileUploads setting
+ in web.xml.
+
+
Settings in web.xml affecting this class :
+
+ - MaxHttpRequestSize
+
- MaxFileUploadRequestSize
+
- MaxRequestParamValueSize (used by {@link hirondelle.web4j.request.RequestParameter})
+
- SpamDetectionInFirewall
+
- FullyValidateFileUploads
+
+
+ The above settings control the validations performed by this class :
+
+
+ | Check |
+ Regular |
+ File Upload (Wrapped) |
+ File Upload |
+
+
+ | Overall request size <= MaxHttpRequestSize |
+ Y |
+ N |
+ N |
+
+
+ | Overall request size <= MaxFileUploadRequestSize |
+ N |
+ Y |
+ Y |
+
+
+ | Every param name is among the {@link hirondelle.web4j.request.RequestParameter}s for that {@link Action} |
+ Y |
+ Y* |
+ N |
+
+
+ | Every param value satifies {@link hirondelle.web4j.request.RequestParameter#isValidParamValue(String)} |
+ Y |
+ Y** |
+ N |
+
+
+ | If created with {@link hirondelle.web4j.request.RequestParameter#withLengthCheck(String)}, then param value size <= MaxRequestParamValueSize |
+ Y |
+ Y** |
+ N |
+
+
+ | If SpamDetectionInFirewall is on, then each param value is checked using the configured {@link hirondelle.web4j.security.SpamDetector} |
+ Y |
+ Y** |
+ N |
+
+
+ | If a request param named Operation exists and it returns true for {@link Operation#hasSideEffects()}, then the underlying request must be a POST |
+ Y |
+ Y |
+ N |
+
+
+ | CSRF Defenses |
+ Y |
+ Y |
+ N |
+
+
+ * For file upload controls, the param name is checked only if the return value of getParameterNames() (for the wrapper) includes it.
+
**Except for file upload controls. For file upload controls, no checks on the param value are made by this class.
+
+
+Defending Against CSRF Attacks
+ If the usual WEB4J defenses against CSRF attacks are active (see package-level comments),
+ then for every POST request executed within a session the following will also be performed as a defense against CSRF attacks :
+
+ - validate that a request parameter named
+ {@link hirondelle.web4j.security.CsrfFilter#FORM_SOURCE_ID_KEY} is present. (This
+ request parameter is deemed to be a special 'internal' parameter, and does not need to be explicitly declared in
+ your Action like other request parameters.)
+
- validate that its value matches items stored in session scope. First check versus an item stored
+ under the key {@link hirondelle.web4j.security.CsrfFilter#FORM_SOURCE_ID_KEY}; if that check fails, then
+ check versus an item stored under the key
+ {@link hirondelle.web4j.security.CsrfFilter#PREVIOUS_FORM_SOURCE_ID_KEY}
+
+
+ See {@link hirondelle.web4j.security.CsrfFilter} for more information.
+*/
+public class ApplicationFirewallImpl implements ApplicationFirewall {
+
+ /** Map actions to expected params. */
+ public static void init(){
+ fExpectedParams = ConfigReader.fetchPublicStaticFinalFields(Action.class, RequestParameter.class);
+ fLogger.config("Expected Request Parameters per Web Action." + Util.logOnePerLine(fExpectedParams));
+ }
+
+ /**
+ Perform checks on the incoming request.
+
+ See class description for more information.
+
+
Subclasses may extend this implementation, following the form :
+
+ public void doHardValidation(Action aAction, RequestParser aRequestParser) throws BadRequestException {
+ super(aAction, aRequestParser);
+ //place additional validations here
+ //for example, one might check that a Content-Length header is present,
+ //or that all header values are within some size range
+ }
+
+ */
+ public void doHardValidation(Action aAction, RequestParser aRequestParser) throws BadRequestException {
+ if( aRequestParser.isFileUploadRequest() ){
+ fLogger.fine("Validating a file upload request.");
+ }
+ checkForExtremeSize(aRequestParser);
+ if ( aRequestParser.isFileUploadRequest() && ! fConfig.getFullyValidateFileUploads() ) {
+ fLogger.fine("Unable to parse request in the usual way: file upload request is not wrapped. Cannot read parameter names and values. See FullyValidateFileUploads setting in web.xml.");
+ }
+ else {
+ checkParamNamesAndValues(aAction, aRequestParser);
+ checkSideEffectOperations(aAction, aRequestParser);
+ defendAgainstCSRFAttacks(aRequestParser);
+ }
+ }
+
+ // PRIVATE
+
+ private Config fConfig = new Config();
+
+ /**
+ Maps {@link Action} classes to a List of expected {@link hirondelle.web4j.request.RequestParameter} objects.
+ If the incoming request contains a request parameter whose name or value is not consistent with this
+ list, then a {@link hirondelle.web4j.model.BadRequestException} is thrown.
+
+ Key - class object
+
Value - Set of RequestParameter objects; may be empty, but not null.
+
+
This is a mutable object field, but is not modified after startup, so this class is thread-safe.
+ */
+ private static Map, Set> fExpectedParams = new LinkedHashMap, Set>();
+
+ /** Special, 'internal' request parameter, used by the framework to defend against CSRF attacks. */
+ private static final RequestParameter fCSRF_REQ_PARAM = RequestParameter.withLengthCheck(CsrfFilter.FORM_SOURCE_ID_KEY);
+
+ private static final String CURRENT_TOKEN_CSRF = CsrfFilter.FORM_SOURCE_ID_KEY;
+ private static final String PREVIOUS_TOKEN_CSRF = CsrfFilter.PREVIOUS_FORM_SOURCE_ID_KEY;
+ private static final Logger fLogger = Util.getLogger(ApplicationFirewallImpl.class);
+
+ /**
+ Some denial-of-service attacks place large amounts of data in the request
+ params, in an attempt to overload the server. This method will check for
+ such requests. This check must be performed first, before any further
+ processing is attempted.
+ */
+ private void checkForExtremeSize(RequestParser aRequest) throws BadRequestException {
+ fLogger.fine("Checking for extreme size.");
+ if ( isRequestExcessivelyLarge(aRequest) ) {
+ throw new BadRequestException(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
+ }
+ }
+
+ private boolean isRequestExcessivelyLarge(RequestParser aRequestParser){
+ boolean result = false;
+ if ( aRequestParser.isFileUploadRequest() ) {
+ result = aRequestParser.getRequest().getContentLength() > fConfig.getMaxFileUploadRequestSize();
+ }
+ else {
+ result = aRequestParser.getRequest().getContentLength() > fConfig.getMaxHttpRequestSize();
+ }
+ return result;
+ }
+
+ void checkParamNamesAndValues(Action aAction, RequestParser aRequestParser) throws BadRequestException {
+ if ( fExpectedParams.containsKey(aAction.getClass()) ){
+ Set expectedParams = fExpectedParams.get(aAction.getClass());
+ //this method may return file upload controls - depends on interpretation, whether to include file upload controls in this method
+ Enumeration paramNames = aRequestParser.getRequest().getParameterNames();
+ while ( paramNames.hasMoreElements() ){
+ String incomingParamName = (String)paramNames.nextElement();
+ fLogger.fine("Checking parameter named " + Util.quote(incomingParamName));
+ RequestParameter knownParam = matchToKnownParam(incomingParamName, expectedParams);
+ if( knownParam == null ){
+ fLogger.severe("*** Unknown Parameter *** : " + Util.quote(incomingParamName) + ". Please add public static final RequestParameter field for this item to your Action.");
+ throw new BadRequestException(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ if ( knownParam.isFileUploadParameter() ) {
+ fLogger.fine("File Upload parameter - value not validatable here: " + knownParam.getName());
+ continue; //prevents checks on values for file upload controls
+ }
+ Collection paramValues = aRequestParser.toSafeTexts(knownParam);
+ if( ! isInternalParam( knownParam) ) {
+ checkParamValues(knownParam, paramValues);
+ }
+ }
+ }
+ else {
+ String message = "Action " + aAction.getClass() + " not known to ApplicationFirewallImpl.";
+ fLogger.severe(message);
+ //this is NOT a BadRequestEx, since not outside the control of this framework.
+ throw new RuntimeException(message);
+ }
+ }
+
+ /** If no match is found, return null. Matches to both regular and 'internal' request params. */
+ private RequestParameter matchToKnownParam(String aIncomingParamName, Collection aExpectedParams){
+ RequestParameter result = null;
+ for (RequestParameter reqParam: aExpectedParams){
+ if ( reqParam.getName().equals(aIncomingParamName) ){
+ result = reqParam;
+ break;
+ }
+ }
+ if( result == null && fCSRF_REQ_PARAM.getName().equals(aIncomingParamName) ){
+ result = fCSRF_REQ_PARAM;
+ }
+ return result;
+ }
+
+ private void checkParamValues(RequestParameter aKnownReqParam, Collection aParamValues) throws BadRequestException {
+ for(SafeText paramValue: aParamValues){
+ if ( Util.textHasContent(paramValue) ) {
+ if ( ! aKnownReqParam.isValidParamValue(paramValue.getRawString()) ) {
+ fLogger.severe("Request parameter named " + aKnownReqParam.getName() + " has an invalid value. Its size is: " + paramValue.getRawString().length());
+ throw new BadRequestException(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ if( fConfig.getSpamDetectionInFirewall() ){
+ SpamDetector spamDetector = BuildImpl.forSpamDetector();
+ if( spamDetector.isSpam(paramValue.getRawString()) ){
+ fLogger.fine("SPAM detected.");
+ throw new BadRequestException(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+ }
+ }
+ }
+
+ private void checkSideEffectOperations(Action aAction, RequestParser aRequestParser) throws BadRequestException {
+ fLogger.fine("Checking for side-effect operations.");
+ Set expectedParams = fExpectedParams.get(aAction.getClass());
+ for (RequestParameter reqParam : expectedParams){
+ if ( "Operation".equals(reqParam.getName()) ){
+ String rawValue = aRequestParser.getRawParamValue(reqParam);
+ if (Util.textHasContent(rawValue)){
+ Operation operation = Operation.valueOf(rawValue);
+ if ( isAttemptingSideEffectOperationWithoutPOST(operation, aRequestParser) ){
+ fLogger.severe("Security problem. Attempted operation having side effects outside of a POST. Please use a