Java tutorial
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ // //// /// /// /// ////// //// // //// //// /// //// //// // //// //// /// //// ///// // /// /// //// ///// // //// ////// // /// ///// // //// //// // //// ///// // //// //// ///////////// //// //// //////////// /// /// ///// ///// //// //// ///// ///// //// ///// // //// //// /// ///// // ///// ///// //////////// //// //// //// //// // The Web framework with class. // ~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Copyright (c) 2013 Adam R. Nelson // // Licensed 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 com.sector91.wit.http; import static com.sector91.wit.html.Builder.*; import java.security.SecureRandom; import java.util.Map; import java.util.Random; /*>>> import checkers.nullness.quals.Nullable;*/ import org.simpleframework.http.Cookie; import org.simpleframework.http.Request; import org.simpleframework.http.Response; import org.simpleframework.http.Status; import com.esotericsoftware.minlog.Log; import com.google.common.io.BaseEncoding; import com.google.common.net.HttpHeaders; import com.sector91.wit.Interceptor; import com.sector91.wit.html.Html; import com.sector91.wit.html.InputEl.Type; /** * <p>Protects non-{@code GET} requests from cross-site request forgery (CSRF) * attacks. This works by generating a random token, which will be stored in * a cookie, then rejecting all non-{@code GET} requests which do not contain * this token by returning an HTTP 403 error. This strategy is based on <a href * ="https://docs.djangoproject.com/en/dev/ref/contrib/csrf/">Django's * CSRF-protection middleware.</a></p> * * <p>In order for this interceptor to work, the token must be obtained with * the static {@link #csrfToken(Request, Response)} method, which also creates * the cookie if it does not already exist. The token must be stored either in * a form field named {@code "csrfToken"} or in an HTTP header named {@code * "X-CSRFToken"}. (The convenience method {@link #csrfTokenField(Request, * Response)} calls {@code csrfToken} and generates an {@code <input * name="csrfToken" type="hidden">} automatically.)</p> * * @author Adam R. Nelson */ public class CsrfInterceptor implements Interceptor { public static final String COOKIE_NAME = "csrftoken"; public static final String FORM_KEY = "csrfToken"; public static final String CSRF_HEADER = "X-CSRFToken"; public static final int CSRF_TOKEN_BITS = 128; private static final String TAG = CsrfInterceptor.class.getSimpleName(); private static final Random rnd = new SecureRandom(); static { Log.debug(TAG, "Seeding SecureRandom for CSRF token generation..."); long start = System.currentTimeMillis(); rnd.setSeed(System.currentTimeMillis()); long timeTaken = System.currentTimeMillis() - start; Log.debug(TAG, "SecureRandom setup finished in " + timeTaken + "ms."); } private static String generateToken() { final byte[] bytes = new byte[CSRF_TOKEN_BITS]; rnd.nextBytes(bytes); return BaseEncoding.base64().encode(bytes); } public static Html csrfTokenField(Request request, Response response) { return input.id(FORM_KEY).name(FORM_KEY).type(Type.HIDDEN).value(csrfToken(request, response)); } public static String csrfToken(Request request, Response response) { final CookieParams params = (CookieParams) request.getAttribute(CookieParams.class); if (params == null) throw new IllegalStateException( "Cannot use CsrfInterceptor.csrfToken()" + " on a route that does not have a CsrfInterceptor."); Cookie cookie = request.getCookie(params.name); String token; if (cookie == null) { // Generate the token if it's not already stored in a cookie. Log.trace(TAG, "Creating new CSRF token."); token = generateToken(); cookie = new Cookie(params.name, token); if (params.domain != null) cookie.setDomain(params.domain); if (params.path != null) cookie.setPath(params.path); cookie.setSecure(params.secure); cookie.setProtected(params.httpOnly); response.setCookie(cookie); } else { token = cookie.getValue(); } response.addValue(HttpHeaders.VARY, "Cookie"); return token; } private final CookieParams cookieParams = new CookieParams(); @Override public void intercept(Request request, Response response) throws HttpException { // Include information on the CSRF token cookie in the request metadata, so // that the csrfToken() method knows what parameters to set on the cookie. final Map<Object, Object> requestAttrs = request.getAttributes(); requestAttrs.put(CookieParams.class, cookieParams); final String method = request.getMethod(); // For "safe" requests (according to RFC 2616), assume no action is // performed and therefore no CSRF protection is needed. if ("GET".equals(method) || "HEAD".equals(method) || "OPTIONS".equals(method) || "TRACE".equals(method)) { return; } // For POST, PUT, DELETE, etc. requests, check that a CSRF token was // received, either in a form field or in an X-CSRFToken header, and // throw an HTTP 403 Forbidden if it was not. final Cookie cookie = request.getCookie(cookieParams.name); if (cookie == null) { Log.debug(TAG, "CSRF check failed: Request does not contain CSRF token cookie."); throw new HttpException(Status.FORBIDDEN); } final String token = cookie.getValue(); if (token.equals(request.getValue(CSRF_HEADER)) || token.equals(request.getParameter(FORM_KEY))) { if (request.isSecure()) { // If we're using HTTPS, an extra check is required to prevent // "man-in-the-middle" attacks. HTTPS requests reliably set the // Referer header, so check that this header is present and that // the request came from this domain. final String referer = request.getValue(HttpHeaders.REFERER); if (referer == null) { Log.debug(TAG, "CSRF check failed: HTTPS request has no referer."); throw new HttpException(Status.FORBIDDEN); } final String safeHost = "https://" + request.getValue(HttpHeaders.HOST); if (!referer.startsWith(safeHost)) { Log.debug(TAG, "CSRF check failed: Referer '" + referer + "' is not in HTTPS domain '" + safeHost + "'."); throw new HttpException(Status.FORBIDDEN); } } } else { Log.debug(TAG, "CSRF check failed: Missing or invalid token."); throw new HttpException(Status.FORBIDDEN); } } public CsrfInterceptor cookieName(String name) { cookieParams.name = name; return this; } public CsrfInterceptor cookieDomain(String domain) { cookieParams.domain = domain; return this; } public CsrfInterceptor cookiePath(String path) { cookieParams.path = path; return this; } public CsrfInterceptor cookieSecure(boolean secure) { cookieParams.secure = secure; return this; } public CsrfInterceptor cookieHttpOnly(boolean httpOnly) { cookieParams.httpOnly = httpOnly; return this; } private static class CookieParams { CookieParams() { } /*@Nullable*/ String name = COOKIE_NAME; /*@Nullable*/ String domain = null; /*@Nullable*/ String path = null; boolean secure = false; boolean httpOnly = true; } }