2010年2月7日 星期日

ESAPI CSRFTokenUtil


ESAPI的CSRFTokenUtil
利用java.security.SecureRandom產生secure random token
提供頁面表單所加入的security token以供後端server進行驗證
除可預防傳統的表單重複傳送,也可以預防CSRF(Cross Site Request Forgery)
而工作上所使用的framework sturts也提供了token的功能,但是review原始碼後可以發現struts2所提供的演算法異常的薄弱=_=(真的很弱),所以建議自行改寫
而OWASP的ESAPI提供的CSRFTokenUtil或許可以補強這塊
其中SecureRandom.getInstance("SHA1PRNG")的SHA1PRNG之前都不曾使用過,便好好的研讀並請教熟密碼學的學長,java doc解釋SecureRandom這class如下:
This class provides a cryptographically strong pseudo-random number generator (PRNG)
關於PRNG的解釋引述IV的解釋如下:
"像 random這樣的參數
在一定次數的抽樣下,
是會有重覆,並且可預測的
所以學術界在研究這塊的
就是研究一個 pseudo-random number的產生器
讓它可以儘量randomn一點
通常random number的產生方法
是先產生一串很長的亂數串列
然後還裏面隨機取出一個來
就是亂數
但是人造的串列,即使很長很長
總是會產出一個cycle
所以研究的人就是想辦法把這個cycle弄長
讓它不會被預測中,並且於cycle中提供一個演算法
讓每個數被抽到的機率是一樣的均勻的抽到一個數
於是密碼學上就要來定義,怎麼樣的pseudo-random number產生器是好的產出器
就有了「strong」pseudo-random number generator
有一些條件要滿足,滿足了這些條件,就是叫作strong pseudo-random number generator沒有滿足的,就只能叫作pseudo-random number generator
而SHA1PRNG 是利用sha1演算法為基礎,來產生 pseudo-random number
所以叫作SHA1PNG"
使用方法
前端jsp頁面的表單中加入input type="hidden"

name=<%= CSRFTokenUtil.SESSION_ATTR_KEY %>
value=<%= CSRFTokenUtil.getToken(request.getSession(false)) %>

後端則使用

if (!CSRFTokenUtil.isValid(request.getSession(false), request)){
return mapping.findForward("error");
}

底下是CSRFUtil的source code



package org.owasp.csrf.util;

import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.ServletException;

/**
* Based on OWASP CryptoUtil.java and CSRFGuard.java written by Eric Sheriden for OWASP
*
* This version by Rohit K. Sethi, Security Compass
* Created Aug. 9, 2007
*
* This is a very basic tool to add a unique token to form fields for
* mitigating against Cross Site Request Forgery (CSRF) attacks.
*
*/


public final class CSRFTokenUtil
{
private final static String DEFAULT_PRNG = "SHA1PRNG"; //algorithm to generate key
public final static String SESSION_ATTR_KEY = "CSRF_TOKEN";
private final static String NO_SESSION_ERROR = "No valid session found";
/**
* Generates a random token to use in CSRF with the default
* Crytopgrahically strong Pseudo-Number Random Generator
*
* @return A string with a random token
* @throws NoSuchAlgorithmException if PRNG algorithm is not valid
*/
private static String getToken() throws NoSuchAlgorithmException
{
return getToken(DEFAULT_PRNG);
}

/**
* Generates a random token to use in CSRF with the default
* Crytopgrahically strong Pseudo-Number Random Generator
*
* @param prng Random Number generator to use
* @return A string with a random token
* @throws NoSuchAlgorithmException if PRNG algorithm is not valid
*/
private static String getToken(String prng) throws NoSuchAlgorithmException
{
SecureRandom sr = SecureRandom.getInstance(prng);
return "" + sr.nextLong();
}


/**
* Retrieves the CSRF token from the current session. Creates a token if one
* does not already exist
* @param session HTTP Session for user - must be valid
* @return token for the session, a new one is created if it doesn't already exist
* @throws ServletException if session is null
* @throws NoSuchAlgorithmException if random number generator algorithm doesn't exist
*/
public static String getToken (HttpSession session) throws ServletException, NoSuchAlgorithmException {
//throw exception if session is null
if (session == null) {
throw new ServletException(NO_SESSION_ERROR);
}

//Now attempt to retrieve existing token from session. If it doesn't exist then
//add it
String token_val = (String)session.getAttribute(SESSION_ATTR_KEY);
if (token_val == null){
token_val = getToken();
session.setAttribute(SESSION_ATTR_KEY, token_val);

}
return token_val;

}

/**
* Tests whether or not the value of the CSRF_TOKEN parameter in the request
* is equal to the value of the CSRF_TOKEN attribute in the session
*
* @param session Session with existing token (will be created if it doesn't exist)
* @param request Inbound HttpRequest that you wish to check if it has a valid
* anti-CSRF token
* @return true if the parameter value matches the token in the session, false otherwise
* @throws ServletException If the session is null
* @throws NoSuchAlgorithmException if random number generator algorithm doesn't exist
*/
public static boolean isValid (HttpSession session, HttpServletRequest request)
throws ServletException, NoSuchAlgorithmException {
//throw exception if session is null
if (session == null) {
throw new ServletException(NO_SESSION_ERROR);
}
return getToken(session).equals(
request.getParameter(SESSION_ATTR_KEY));
}

}


這裡可以看到sturts所提供的token產生方式

return new BigInteger(165, RANDOM).toString(36).toUpperCase();




/*
* $Id: TokenHelper.java 781798 2009-06-04 17:08:35Z wesw $
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.struts2.util;

import java.math.BigInteger;
import java.util.Map;
import java.util.Random;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.LocalizedTextUtil;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

/**
* TokenHelper
*
*/
public class TokenHelper {

/**
* The default name to map the token value
*/
public static final String DEFAULT_TOKEN_NAME = "struts.token";

/**
* The name of the field which will hold the token name
*/
public static final String TOKEN_NAME_FIELD = "struts.token.name";
private static final Logger LOG = LoggerFactory.getLogger(TokenHelper.class);
private static final Random RANDOM = new Random();


/**
* Sets a transaction token into the session using the default token name.
*
* @return the token string
*/
public static String setToken() {
return setToken(DEFAULT_TOKEN_NAME);
}

/**
* Sets a transaction token into the session using the provided token name.
*
* @param tokenName the name to store into the session with the token as the value
* @return the token string
*/
public static String setToken(String tokenName) {
Map session = ActionContext.getContext().getSession();
String token = generateGUID();
try {
session.put(tokenName, token);
}
catch(IllegalStateException e) {
// WW-1182 explain to user what the problem is
String msg = "Error creating HttpSession due response is commited to client. You can use the CreateSessionInterceptor or create the HttpSession from your action before the result is rendered to the client: " + e.getMessage();
LOG.error(msg, e);
throw new IllegalArgumentException(msg);
}

return token;
}


/**
* Gets a transaction token into the session using the default token name.
*
* @return token
*/
public static String getToken() {
return getToken(DEFAULT_TOKEN_NAME);
}

/**
* Gets the Token value from the params in the ServletActionContext using the given name
*
* @param tokenName the name of the parameter which holds the token value
* @return the token String or null, if the token could not be found
*/
public static String getToken(String tokenName) {
if (tokenName == null ) {
return null;
}
Map params = ActionContext.getContext().getParameters();
String[] tokens = (String[]) params.get(tokenName);
String token;

if ((tokens == null) || (tokens.length < 1)) {
LOG.warn("Could not find token mapped to token name " + tokenName);

return null;
}

token = tokens[0];

return token;
}

/**
* Gets the token name from the Parameters in the ServletActionContext
*
* @return the token name found in the params, or null if it could not be found
*/
public static String getTokenName() {
Map params = ActionContext.getContext().getParameters();

if (!params.containsKey(TOKEN_NAME_FIELD)) {
LOG.warn("Could not find token name in params.");

return null;
}

String[] tokenNames = (String[]) params.get(TOKEN_NAME_FIELD);
String tokenName;

if ((tokenNames == null) || (tokenNames.length < 1)) {
LOG.warn("Got a null or empty token name.");

return null;
}

tokenName = tokenNames[0];

return tokenName;
}

/**
* Checks for a valid transaction token in the current request params. If a valid token is found, it is
* removed so the it is not valid again.
*
* @return false if there was no token set into the params (check by looking for {@link #TOKEN_NAME_FIELD}), true if a valid token is found
*/
public static boolean validToken() {
String tokenName = getTokenName();

if (tokenName == null) {
if (LOG.isDebugEnabled())
LOG.debug("no token name found -> Invalid token ");
return false;
}

String token = getToken(tokenName);

if (token == null) {
if (LOG.isDebugEnabled())
LOG.debug("no token found for token name "+tokenName+" -> Invalid token ");
return false;
}

Map session = ActionContext.getContext().getSession();
String sessionToken = (String) session.get(tokenName);

if (!token.equals(sessionToken)) {
LOG.warn(LocalizedTextUtil.findText(TokenHelper.class, "struts.internal.invalid.token", ActionContext.getContext().getLocale(), "Form token {0} does not match the session token {1}.", new Object[]{
token, sessionToken
}));

return false;
}

// remove the token so it won't be used again
session.remove(tokenName);

return true;
}

public static String generateGUID() {
return new BigInteger(165, RANDOM).toString(36).toUpperCase();
}
}

沒有留言: