Coverage Report - net.sourceforge.pebble.web.security.SecurityTokenValidatorImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
SecurityTokenValidatorImpl
24%
21/87
15%
7/46
7
 
 1  
 /*
 2  
  * Copyright (c) 2003-2011, Simon Brown
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions are met:
 7  
  *
 8  
  *   - Redistributions of source code must retain the above copyright
 9  
  *     notice, this list of conditions and the following disclaimer.
 10  
  *
 11  
  *   - Redistributions in binary form must reproduce the above copyright
 12  
  *     notice, this list of conditions and the following disclaimer in
 13  
  *     the documentation and/or other materials provided with the
 14  
  *     distribution.
 15  
  *
 16  
  *   - Neither the name of Pebble nor the names of its contributors may
 17  
  *     be used to endorse or promote products derived from this software
 18  
  *     without specific prior written permission.
 19  
  *
 20  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 21  
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 22  
  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 23  
  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 24  
  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 25  
  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 26  
  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 27  
  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 28  
  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 29  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 30  
  * POSSIBILITY OF SUCH DAMAGE.
 31  
  */
 32  
 package net.sourceforge.pebble.web.security;
 33  
 
 34  
 import net.sourceforge.pebble.Constants;
 35  
 import net.sourceforge.pebble.domain.AbstractBlog;
 36  
 import net.sourceforge.pebble.domain.Blog;
 37  
 import net.sourceforge.pebble.web.action.Action;
 38  
 import org.apache.commons.codec.binary.Base64;
 39  
 import org.springframework.stereotype.Component;
 40  
 
 41  
 import javax.servlet.http.Cookie;
 42  
 import javax.servlet.http.HttpServletRequest;
 43  
 import javax.servlet.http.HttpServletResponse;
 44  
 import java.net.URLEncoder;
 45  
 import java.security.MessageDigest;
 46  
 import java.security.NoSuchAlgorithmException;
 47  
 import java.security.SecureRandom;
 48  
 import java.util.*;
 49  
 
 50  
 /**
 51  
  * Checks requests for a security token
 52  
  *
 53  
  * @author James Roper
 54  
  */
 55  
 @Component
 56  4
 public class SecurityTokenValidatorImpl implements SecurityTokenValidator {
 57  
 
 58  
   /**
 59  
    * the security token name
 60  
    */
 61  
   public static final String PEBBLE_SECURITY_TOKEN_PARAMETER = "pebbleSecurityToken";
 62  
 
 63  
   /**
 64  
    * the parameter for the hash, this is used for links that aren't from web pages (eg emails)
 65  
    */
 66  
   public static final String PEBBLE_SECURITY_SIGNATURE_PARAMETER = "pebbleSecurityHash";
 67  
 
 68  
   /**
 69  
    * the header for bypassing security token checks
 70  
    */
 71  
   private static final String PEBBLE_SECURITY_TOKEN_HEADER = "X-Pebble-Token";
 72  
 
 73  
   /**
 74  
    * the value the header should be for not checking
 75  
    */
 76  
   private static final String PEBBLE_SECURITY_TOKEN_HEADER_NOCHECK = "nocheck";
 77  
 
 78  
   /**
 79  
    * For generating secure tokens
 80  
    */
 81  4
   private static final SecureRandom random = new SecureRandom();
 82  
 
 83  
   /**
 84  
    * Validate the security token for this request, if necessary, setting up the security token cookie if it doesn't
 85  
    * exist
 86  
    *
 87  
    * @param request  The request to validate
 88  
    * @param response The response
 89  
    * @param action   The action to validate
 90  
    * @return true if the request can proceed, false if not
 91  
    */
 92  
   public boolean validateSecurityToken(HttpServletRequest request, HttpServletResponse response, Action action) {
 93  
     // First, ensure that there is a security token, for future requests
 94  0
     String token = ensureSecurityTokenExists(request, response);
 95  0
     if (shouldValidate(action, request)) {
 96  
       // Check for the header is there... XSRF attacks can't set custom headers, so if this header is there,
 97  
       // it must be safe
 98  0
       if (PEBBLE_SECURITY_TOKEN_HEADER_NOCHECK.equals(request.getHeader(PEBBLE_SECURITY_TOKEN_HEADER))) {
 99  0
         return true;
 100  
       }
 101  
       // We must validate the token
 102  0
       String requestToken = request.getParameter(PEBBLE_SECURITY_TOKEN_PARAMETER);
 103  
       // Compare token to cookie
 104  0
       if (token.equals(requestToken)) {
 105  0
         return true;
 106  
       }
 107  
       // No token, try validating if the request is signed
 108  0
       return validateSignedRequest(request);
 109  
     } else {
 110  0
       return true;
 111  
     }
 112  
   }
 113  
 
 114  
   private boolean shouldValidate(Action action, HttpServletRequest request) {
 115  0
     RequireSecurityToken annotation = action.getClass().getAnnotation(RequireSecurityToken.class);
 116  0
     if (annotation != null) {
 117  
       // Check for a condition
 118  0
       Class<? extends SecurityTokenValidatorCondition> condition = annotation.value();
 119  0
       if (condition != null && condition != NullSecurityTokenValidatorCondition.class) {
 120  
         // Instantiate condition
 121  
         try {
 122  0
           return condition.newInstance().shouldValidate(request);
 123  0
         } catch (IllegalAccessException iae) {
 124  0
           throw new RuntimeException("Could not instantiate " + condition);
 125  0
         } catch (InstantiationException ie) {
 126  0
           throw new RuntimeException("Could not instantiate " + condition);
 127  
         }
 128  
       }
 129  
       // Otherwise, with no condition we should return validate
 130  0
       return true;
 131  
     } else {
 132  
       // We have no annotation, don't validate
 133  0
       return false;
 134  
     }
 135  
   }
 136  
 
 137  
   private String ensureSecurityTokenExists(HttpServletRequest request, HttpServletResponse response) {
 138  0
     String token = (String) request.getAttribute(PEBBLE_SECURITY_TOKEN_PARAMETER);
 139  0
     if (token != null) {
 140  
       // We've already configured it for this request
 141  0
       return token;
 142  
     }
 143  0
     Cookie[] cookies = request.getCookies();
 144  0
     if (cookies != null) {
 145  0
       for (Cookie cookie : cookies) {
 146  0
         if (PEBBLE_SECURITY_TOKEN_PARAMETER.equals(cookie.getName())) {
 147  0
           token = cookie.getValue();
 148  
         }
 149  
       }
 150  
     }
 151  
     // No cookie, generate a token at least 12 characters long
 152  0
     if (token == null) {
 153  0
       String contextPath = request.getContextPath();
 154  
       // Ensure context path is not empty
 155  0
       if (contextPath == null || contextPath.length() == 0) {
 156  0
         contextPath = "/";
 157  
       }
 158  0
       token = "";
 159  0
       while (token.length() < 12) {
 160  0
         token += Long.toHexString(random.nextLong());
 161  
       }
 162  
       // Set the cookie
 163  0
       Cookie cookie = new Cookie(PEBBLE_SECURITY_TOKEN_PARAMETER, token);
 164  
       // Non persistent
 165  0
       cookie.setMaxAge(-1);
 166  0
       cookie.setPath(contextPath);
 167  0
       response.addCookie(cookie);
 168  
     }
 169  
     // Set it as a request attribute so the security token tag can find it
 170  0
     request.setAttribute(PEBBLE_SECURITY_TOKEN_PARAMETER, token);
 171  0
     return token;
 172  
   }
 173  
 
 174  
   private boolean validateSignedRequest(HttpServletRequest request) {
 175  0
     String requestHash = request.getParameter(PEBBLE_SECURITY_SIGNATURE_PARAMETER);
 176  0
     if (requestHash != null) {
 177  0
       AbstractBlog blog = (AbstractBlog) request.getAttribute(Constants.BLOG_KEY);
 178  0
       if (blog instanceof Blog) {
 179  0
         String salt = ((Blog) blog).getXsrfSigningSalt();
 180  
         // Convert request parameters to map
 181  0
         String servletPath = request.getServletPath();
 182  0
         if (servletPath.startsWith("/")) {
 183  0
           servletPath = servletPath.substring(1);
 184  
         }
 185  0
         String hash = hashRequest(servletPath, request.getParameterMap(), salt);
 186  0
         return hash.equals(requestHash);
 187  
       }
 188  
     }
 189  0
     return false;
 190  
   }
 191  
 
 192  
   /**
 193  
    * Hashes the given query parameters by sorting the keys alphabetically and then hashing the & separated query String
 194  
    * that would be generated by having the keys in that order, concatinated with the salt
 195  
    *
 196  
    * @param params The parameters in the query String
 197  
    * @param salt   The secret salt
 198  
    * @return The hash in base64
 199  
    */
 200  
   public String hashRequest(String servletPath, Map<String, String[]> params, String salt) {
 201  12
     List<String> keys = new ArrayList<String>(params.keySet());
 202  12
     Collections.sort(keys);
 203  
 
 204  
     MessageDigest digest;
 205  
     try {
 206  12
       digest = MessageDigest.getInstance("MD5");
 207  0
     } catch (NoSuchAlgorithmException e) {
 208  0
       throw new RuntimeException(e);
 209  12
     }
 210  12
     digest.update(servletPath.getBytes());
 211  12
     digest.update((byte) '?');
 212  12
     boolean start = true;
 213  12
     for (String key : keys) {
 214  24
       if (!key.equals(PEBBLE_SECURITY_SIGNATURE_PARAMETER)) {
 215  48
         for (String value : params.get(key)) {
 216  24
           if (!start) {
 217  12
             digest.update((byte) '&');
 218  
           }
 219  24
           start = false;
 220  24
           digest.update(key.getBytes());
 221  24
           digest.update((byte) '=');
 222  24
           digest.update(value.getBytes());
 223  
         }
 224  
       }
 225  
     }
 226  12
     digest.update(salt.getBytes());
 227  12
     byte[] hash = digest.digest();
 228  12
     return new String(Base64.encodeBase64(hash, false));
 229  
   }
 230  
 
 231  
   /**
 232  
    * Generate a signed query string
 233  
    *
 234  
    * @param params The parameters in the query string.  This method assumes the parameters are not URL encoded
 235  
    * @param salt   The salt to sign it with
 236  
    * @return The HTML escaped signed query string
 237  
    */
 238  
   public String generateSignedQueryString(String servletPath, Map<String, String[]> params, String salt) {
 239  0
     String hash = hashRequest(servletPath, params, salt);
 240  0
     StringBuilder url = new StringBuilder(servletPath);
 241  0
     String sep = "?";
 242  0
     for (Map.Entry<String, String[]> param : params.entrySet()) {
 243  0
       for (String value : param.getValue()) {
 244  0
         url.append(sep);
 245  0
         sep = "&amp;";
 246  0
         url.append(URLEncoder.encode(param.getKey()));
 247  0
         url.append("=");
 248  0
         url.append(URLEncoder.encode(value));
 249  
       }
 250  
     }
 251  0
     url.append(sep).append(PEBBLE_SECURITY_SIGNATURE_PARAMETER).append("=").append(URLEncoder.encode(hash));
 252  0
     return url.toString();
 253  
   }
 254  
 
 255  
 }