/** * The OWASP CSRFGuard Project, BSD License Eric Sheridan * (eric@infraredsecurity.com), Copyright (c) 2011 All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. 2. Redistributions in * binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other * materials provided with the distribution. 3. Neither the name of OWASP nor * the names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.owasp.csrfguard; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.owasp.csrfguard.action.IAction; import org.owasp.csrfguard.config.ConfigurationProvider; import org.owasp.csrfguard.config.ConfigurationProviderFactory; import org.owasp.csrfguard.config.NullConfigurationProvider; import org.owasp.csrfguard.config.PropertiesConfigurationProvider; import org.owasp.csrfguard.config.overlay.ExpirableCache; import org.owasp.csrfguard.log.ILogger; import org.owasp.csrfguard.log.LogLevel; import org.owasp.csrfguard.servlet.JavaScriptServlet; import org.owasp.csrfguard.util.CsrfGuardUtils; import org.owasp.csrfguard.util.RandomGenerator; import org.owasp.csrfguard.util.Streams; import org.owasp.csrfguard.util.Writers; public final class CsrfGuard { public final static String PAGE_TOKENS_KEY = "Owasp_CsrfGuard_Pages_Tokens_Key"; private Properties properties = null; /** * cache the configuration for a minute */ private static ExpirableCache<Boolean, ConfigurationProvider> configurationProviderExpirableCache = new ExpirableCache<Boolean, ConfigurationProvider>(1); private ConfigurationProvider config() { if (this.properties == null) { return new NullConfigurationProvider(); } ConfigurationProvider configurationProvider = configurationProviderExpirableCache.get(Boolean.TRUE); synchronized (CsrfGuard.class) { if (configurationProvider == null) { configurationProvider = retrieveNewConfig(); } else if (!configurationProvider.isCacheable()) { //dont synchronize if not cacheable configurationProvider = retrieveNewConfig(); } } return configurationProvider; } /** * @return new provider */ private ConfigurationProvider retrieveNewConfig() { ConfigurationProvider configurationProvider = null; //lets see what provider we are using String configurationProviderFactoryClassName = this.properties.getProperty( "org.owasp.csrfguard.configuration.provider.factory", PropertiesConfigurationProvider.class.getName()); Class<ConfigurationProviderFactory> configurationProviderFactoryClass = CsrfGuardUtils.forName(configurationProviderFactoryClassName); ConfigurationProviderFactory configurationProviderFactory = CsrfGuardUtils.newInstance(configurationProviderFactoryClass); configurationProvider = configurationProviderFactory.retrieveConfiguration(this.properties); configurationProviderExpirableCache.put(Boolean.TRUE, configurationProvider); return configurationProvider; } private static class SingletonHolder { public static final CsrfGuard instance = new CsrfGuard(); } public static CsrfGuard getInstance() { return SingletonHolder.instance; } public static void load(Properties theProperties) throws NoSuchAlgorithmException, InstantiationException, IllegalAccessException, ClassNotFoundException, IOException, NoSuchProviderException { getInstance().properties = theProperties; } public CsrfGuard() { } public ILogger getLogger() { return config().getLogger(); } public String getTokenName() { return config().getTokenName(); } public int getTokenLength() { return config().getTokenLength(); } public boolean isRotateEnabled() { return config().isRotateEnabled(); } public boolean isTokenPerPageEnabled() { return config().isTokenPerPageEnabled(); } public boolean isTokenPerPagePrecreate() { return config().isTokenPerPagePrecreateEnabled(); } /** * If csrf guard filter should check even if there is no session for the * user Note: this changed in 2014/04/20, the default behavior used to be to * not check if there is no session. If you want the legacy behavior (if * your app is not susceptible to CSRF if the user has no session), set this * to false * * @return if true */ public boolean isValidateWhenNoSessionExists() { return config().isValidateWhenNoSessionExists(); } public SecureRandom getPrng() { return config().getPrng(); } public String getNewTokenLandingPage() { return config().getNewTokenLandingPage(); } public boolean isUseNewTokenLandingPage() { return config().isUseNewTokenLandingPage(); } public boolean isAjaxEnabled() { return config().isAjaxEnabled(); } public boolean isProtectEnabled() { return config().isProtectEnabled(); } /** * @see ConfigurationProvider#isEnabled() * @return if enabled */ public boolean isEnabled() { return config().isEnabled(); } public String getSessionKey() { return config().getSessionKey(); } public Set<String> getProtectedPages() { return config().getProtectedPages(); } public Set<String> getUnprotectedPages() { return config().getUnprotectedPages(); } /** * cache regex patterns here */ private Map<String, Pattern> regexPatternCache = new HashMap<String, Pattern>(); public Set<String> getProtectedMethods() { return config().getProtectedMethods(); } public List<IAction> getActions() { return config().getActions(); } public String getJavascriptSourceFile() { return config().getJavascriptSourceFile(); } /** * @see ConfigurationProvider#isJavascriptInjectFormAttributes() * @return if inject */ public boolean isJavascriptInjectFormAttributes() { return config().isJavascriptInjectFormAttributes(); } /** * @see ConfigurationProvider#isJavascriptInjectGetForms() * @return if inject */ public boolean isJavascriptInjectGetForms() { return config().isJavascriptInjectGetForms(); } public boolean isJavascriptDomainStrict() { return config().isJavascriptDomainStrict(); } public boolean isJavascriptRefererMatchDomain() { return config().isJavascriptRefererMatchDomain(); } public String getJavascriptCacheControl() { return config().getJavascriptCacheControl(); } public Pattern getJavascriptRefererPattern() { return config().getJavascriptRefererPattern(); } public boolean isJavascriptInjectIntoForms() { return config().isJavascriptInjectIntoForms(); } public boolean isJavascriptInjectIntoAttributes() { return config().isJavascriptInjectIntoAttributes(); } public String getJavascriptXrequestedWith() { return config().getJavascriptXrequestedWith(); } public String getJavascriptTemplateCode() { return config().getJavascriptTemplateCode(); } public String getTokenValue(HttpServletRequest request) { return getTokenValue(request, request.getRequestURI()); } public String getTokenValue(HttpServletRequest request, String uri) { String tokenValue = null; HttpSession session = request.getSession(false); if (session != null) { if (isTokenPerPageEnabled()) { @SuppressWarnings("unchecked") Map<String, String> pageTokens = (Map<String, String>) session.getAttribute(CsrfGuard.PAGE_TOKENS_KEY); if (pageTokens != null) { if (isTokenPerPagePrecreate()) { createPageToken(pageTokens, uri); } tokenValue = pageTokens.get(uri); } } if (tokenValue == null) { tokenValue = (String) session.getAttribute(getSessionKey()); } } return tokenValue; } public boolean isValidRequest(HttpServletRequest request, HttpServletResponse response) { boolean valid = !isProtectedPageAndMethod(request); HttpSession session = request.getSession(true); String tokenFromSession = (String) session.getAttribute(getSessionKey()); /** * sending request to protected resource - verify token * */ if (tokenFromSession != null && !valid) { try { if (isAjaxEnabled() && isAjaxRequest(request)) { verifyAjaxToken(request); } else if (isTokenPerPageEnabled()) { verifyPageToken(request); } else { verifySessionToken(request); } valid = true; } catch (CsrfGuardException csrfe) { callActionsOnError(request, response, csrfe); } /** * rotate session and page tokens * */ if (!isAjaxRequest(request) && isRotateEnabled()) { rotateTokens(request); } /** * expected token in session - bad state and not valid * */ } else if (tokenFromSession == null && !valid) { try { throw new CsrfGuardException("CsrfGuard expects the token to exist in session at this point"); } catch (CsrfGuardException csrfe) { callActionsOnError(request, response, csrfe); } } else { /** * unprotected page - nothing to do * */ } return valid; } /** * @param request * @param response * @param csrfe */ private void callActionsOnError(HttpServletRequest request, HttpServletResponse response, CsrfGuardException csrfe) { for (IAction action : getActions()) { try { action.execute(request, response, csrfe, this); } catch (CsrfGuardException exception) { getLogger().log(LogLevel.Error, exception); } } } public void updateToken(HttpSession session) { String tokenValue = (String) session.getAttribute(getSessionKey()); /** * Generate a new token and store it in the session. * */ if (tokenValue == null) { try { tokenValue = RandomGenerator.generateRandomId(getPrng(), getTokenLength()); } catch (Exception e) { throw new RuntimeException(String.format("unable to generate the random token - %s", e.getLocalizedMessage()), e); } session.setAttribute(getSessionKey(), tokenValue); } } public void updateTokens(HttpServletRequest request) { /** * cannot create sessions if response already committed * */ HttpSession session = request.getSession(false); if (session != null) { /** * create master token if it does not exist * */ updateToken(session); /** * create page specific token * */ if (isTokenPerPageEnabled()) { @SuppressWarnings("unchecked") Map<String, String> pageTokens = (Map<String, String>) session.getAttribute(CsrfGuard.PAGE_TOKENS_KEY); /** * first time initialization * */ if (pageTokens == null) { pageTokens = new HashMap<String, String>(); session.setAttribute(CsrfGuard.PAGE_TOKENS_KEY, pageTokens); } /** * create token if it does not exist * */ if (isProtectedPageAndMethod(request)) { createPageToken(pageTokens, request.getRequestURI()); } } } } /** * Create page token if it doesn't exist. * * @param pageTokens A map of tokens. If token doesn't exist it will be * added. * @param uri The key for the tokens. */ private void createPageToken(Map<String, String> pageTokens, String uri) { if (pageTokens == null) { return; } /** * create token if it does not exist * */ if (pageTokens.containsKey(uri)) { return; } try { pageTokens.put(uri, RandomGenerator.generateRandomId(getPrng(), getTokenLength())); } catch (Exception e) { throw new RuntimeException(String.format("unable to generate the random token - %s", e.getLocalizedMessage()), e); } } public void writeLandingPage(HttpServletRequest request, HttpServletResponse response) throws IOException { String landingPage = getNewTokenLandingPage(); /** * default to current page * */ if (landingPage == null) { StringBuilder sb = new StringBuilder(); sb.append(request.getContextPath()); sb.append(request.getServletPath()); landingPage = sb.toString(); } /** * create auto posting form * */ StringBuilder sb = new StringBuilder(); sb.append("<html>\r\n"); sb.append("<head>\r\n"); sb.append("<title>OWASP CSRFGuard Project - New Token Landing Page</title>\r\n"); sb.append("</head>\r\n"); sb.append("<body>\r\n"); sb.append("<script type=\"text/javascript\">\r\n"); sb.append("var form = document.createElement(\"form\");\r\n"); sb.append("form.setAttribute(\"method\", \"post\");\r\n"); sb.append("form.setAttribute(\"action\", \""); sb.append(landingPage); sb.append("\");\r\n"); /** * only include token if needed * */ if (isProtectedPage(landingPage)) { sb.append("var hiddenField = document.createElement(\"input\");\r\n"); sb.append("hiddenField.setAttribute(\"type\", \"hidden\");\r\n"); sb.append("hiddenField.setAttribute(\"name\", \""); sb.append(getTokenName()); sb.append("\");\r\n"); sb.append("hiddenField.setAttribute(\"value\", \""); sb.append(getTokenValue(request, landingPage)); sb.append("\");\r\n"); sb.append("form.appendChild(hiddenField);\r\n"); } sb.append("document.body.appendChild(form);\r\n"); sb.append("form.submit();\r\n"); sb.append("</script>\r\n"); sb.append("</body>\r\n"); sb.append("</html>\r\n"); String code = sb.toString(); /** * setup headers * */ response.setContentType("text/html"); response.setContentLength(code.length()); /** * write auto posting form * */ OutputStream output = null; PrintWriter writer = null; try { output = response.getOutputStream(); writer = new PrintWriter(output); writer.write(code); writer.flush(); } finally { Writers.close(writer); Streams.close(output); } } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("\r\n*****************************************************\r\n"); sb.append("* Owasp.CsrfGuard Properties\r\n"); sb.append("*\r\n"); sb.append(String.format("* Logger: %s\r\n", getLogger().getClass().getName())); sb.append(String.format("* NewTokenLandingPage: %s\r\n", getNewTokenLandingPage())); sb.append(String.format("* PRNG: %s\r\n", getPrng().getAlgorithm())); sb.append(String.format("* SessionKey: %s\r\n", getSessionKey())); sb.append(String.format("* TokenLength: %s\r\n", getTokenLength())); sb.append(String.format("* TokenName: %s\r\n", getTokenName())); sb.append(String.format("* Ajax: %s\r\n", isAjaxEnabled())); sb.append(String.format("* Rotate: %s\r\n", isRotateEnabled())); sb.append(String.format("* Javascript cache control: %s\r\n", getJavascriptCacheControl())); sb.append(String.format("* Javascript domain strict: %s\r\n", isJavascriptDomainStrict())); sb.append(String.format("* Javascript inject attributes: %s\r\n", isJavascriptInjectIntoAttributes())); sb.append(String.format("* Javascript inject forms: %s\r\n", isJavascriptInjectIntoForms())); sb.append(String.format("* Javascript referer pattern: %s\r\n", getJavascriptRefererPattern())); sb.append(String.format("* Javascript referer match domain: %s\r\n", isJavascriptRefererMatchDomain())); sb.append(String.format("* Javascript source file: %s\r\n", getJavascriptSourceFile())); sb.append(String.format("* Javascript X requested with: %s\r\n", getJavascriptXrequestedWith())); sb.append(String.format("* Protected methods: %s\r\n", CsrfGuardUtils.toStringForLog(getProtectedMethods()))); sb.append(String.format("* Protected pages size: %s\r\n", CsrfGuardUtils.length(getProtectedPages()))); sb.append(String.format("* Unprotected methods: %s\r\n", CsrfGuardUtils.toStringForLog(getUnprotectedMethods()))); sb.append(String.format("* Unprotected pages size: %s\r\n", CsrfGuardUtils.length(getUnprotectedPages()))); sb.append(String.format("* TokenPerPage: %s\r\n", isTokenPerPageEnabled())); sb.append(String.format("* Enabled: %s\r\n", isEnabled())); sb.append(String.format("* ValidateWhenNoSessionExists: %s\r\n", isValidateWhenNoSessionExists())); for (IAction action : getActions()) { sb.append(String.format("* Action: %s\r\n", action.getClass().getName())); for (String name : action.getParameterMap().keySet()) { String value = action.getParameter(name); sb.append(String.format("*\tParameter: %s = %s\r\n", name, value)); } } sb.append("*****************************************************\r\n"); return sb.toString(); } private boolean isAjaxRequest(HttpServletRequest request) { String ajaxHeader = request.getHeader("X-Requested-With"); return ajaxHeader != null && ajaxHeader.contains("XMLHttpRequest"); // CUSTOM: added fix to support native android browser } private void verifyAjaxToken(HttpServletRequest request) throws CsrfGuardException { HttpSession session = request.getSession(true); String tokenFromSession = (String) session.getAttribute(getSessionKey()); String tokenFromRequest = request.getHeader(getTokenName()); if (tokenFromRequest == null) { /** * FAIL: token is missing from the request * */ throw new CsrfGuardException("required token is missing from the request"); } else { //if there are two headers, then the result is comma separated if (!tokenFromSession.equals(tokenFromRequest)) { if (tokenFromRequest.contains(",")) { tokenFromRequest = tokenFromRequest.substring(0, tokenFromRequest.indexOf(',')).trim(); } if (!tokenFromSession.equals(tokenFromRequest)) { /** * FAIL: the request token does not match the session token * */ throw new CsrfGuardException("request token does not match session token"); } } } } private void verifyPageToken(HttpServletRequest request) throws CsrfGuardException { HttpSession session = request.getSession(true); @SuppressWarnings("unchecked") Map<String, String> pageTokens = (Map<String, String>) session.getAttribute(CsrfGuard.PAGE_TOKENS_KEY); String tokenFromPages = (pageTokens != null ? pageTokens.get(request.getRequestURI()) : null); String tokenFromSession = (String) session.getAttribute(getSessionKey()); String tokenFromRequest = request.getParameter(getTokenName()); if (tokenFromRequest == null) { /** * FAIL: token is missing from the request * */ throw new CsrfGuardException("required token is missing from the request"); } else if (tokenFromPages != null) { if (!tokenFromPages.equals(tokenFromRequest)) { /** * FAIL: request does not match page token * */ throw new CsrfGuardException("request token does not match page token"); } } else if (!tokenFromSession.equals(tokenFromRequest)) { /** * FAIL: the request token does not match the session token * */ throw new CsrfGuardException("request token does not match session token"); } } private void verifySessionToken(HttpServletRequest request) throws CsrfGuardException { HttpSession session = request.getSession(true); String tokenFromSession = (String) session.getAttribute(getSessionKey()); String tokenFromRequest = request.getParameter(getTokenName()); if (tokenFromRequest == null) { /** * FAIL: token is missing from the request * */ throw new CsrfGuardException("required token is missing from the request"); } else if (!tokenFromSession.equals(tokenFromRequest)) { /** * FAIL: the request token does not match the session token * */ throw new CsrfGuardException("request token does not match session token"); } } private void rotateTokens(HttpServletRequest request) { HttpSession session = request.getSession(true); /** * rotate master token * */ String tokenFromSession = null; try { tokenFromSession = RandomGenerator.generateRandomId(getPrng(), getTokenLength()); } catch (Exception e) { throw new RuntimeException(String.format("unable to generate the random token - %s", e.getLocalizedMessage()), e); } session.setAttribute(getSessionKey(), tokenFromSession); /** * rotate page token * */ if (isTokenPerPageEnabled()) { @SuppressWarnings("unchecked") Map<String, String> pageTokens = (Map<String, String>) session.getAttribute(CsrfGuard.PAGE_TOKENS_KEY); try { pageTokens.put(request.getRequestURI(), RandomGenerator.generateRandomId(getPrng(), getTokenLength())); } catch (Exception e) { throw new RuntimeException(String.format("unable to generate the random token - %s", e.getLocalizedMessage()), e); } } } public boolean isProtectedPage(String uri) { //if this is a javascript page, let it go through if (JavaScriptServlet.getJavascriptUris().contains(uri)) { return false; } boolean retval = !isProtectEnabled(); for (String protectedPage : getProtectedPages()) { if (isUriExactMatch(protectedPage, uri)) { return true; } else if (isUriMatch(protectedPage, uri)) { retval = true; } } for (String unprotectedPage : getUnprotectedPages()) { if (isUriExactMatch(unprotectedPage, uri)) { return false; } else if (isUriMatch(unprotectedPage, uri)) { retval = false; } } return retval; } /** * if the HTTP method is protected, i.e. should be checked for token * * @param method * @return if protected */ public boolean isProtectedMethod(String method) { boolean isProtected = true; { Set<String> theProtectedMethods = getProtectedMethods(); if (!theProtectedMethods.isEmpty() && !theProtectedMethods.contains(method)) { isProtected = false; } } { Set<String> theUnprotectedMethods = getUnprotectedMethods(); if (!theUnprotectedMethods.isEmpty() && theUnprotectedMethods.contains(method)) { isProtected = false; } } return isProtected; } public boolean isProtectedPageAndMethod(String page, String method) { return (isProtectedPage(page) && isProtectedMethod(method)); } public boolean isProtectedPageAndMethod(HttpServletRequest request) { return isProtectedPageAndMethod(request.getRequestURI(), request.getMethod()); } public boolean isPrintConfig() { return config().isPrintConfig(); } /** * FIXME: taken from Tomcat - ApplicationFilterFactory * * @param testPath the pattern to match. * @param requestPath the current request path. * @return {@code true} if {@code requestPath} matches {@code testPath}. */ private boolean isUriMatch(String testPath, String requestPath) { //case 4, if it is a regex if (isTestPathRegex(testPath)) { Pattern pattern = this.regexPatternCache.get(testPath); if (pattern == null) { pattern = Pattern.compile(testPath); this.regexPatternCache.put(testPath, pattern); } return pattern.matcher(requestPath).matches(); } boolean retval = false; /** * Case 1: Exact Match MCH 140419: ??? isnt this checks in * isUriExactMatch() ??? * */ if (testPath.equals(requestPath)) { retval = true; } /** * Case 2 - Path Match ("/.../*") * */ if (testPath.equals("/*")) { retval = true; } if (testPath.endsWith("/*")) { if (testPath .regionMatches(0, requestPath, 0, testPath.length() - 2)) { if (requestPath.length() == (testPath.length() - 2)) { retval = true; } else if ('/' == requestPath.charAt(testPath.length() - 2)) { retval = true; } } } /** * Case 3 - Extension Match * */ if (testPath.startsWith("*.")) { int slash = requestPath.lastIndexOf('/'); int period = requestPath.lastIndexOf('.'); if ((slash >= 0) && (period > slash) && (period != requestPath.length() - 1) && ((requestPath.length() - period) == (testPath.length() - 1))) { retval = testPath.regionMatches(2, requestPath, period + 1, testPath.length() - 2); } } return retval; } /** * see if a test path starts with ^ and ends with $ thus making it a regex * * @param testPath * @return true if regex */ private static boolean isTestPathRegex(String testPath) { return testPath != null && testPath.startsWith("^") && testPath.endsWith("$"); } private boolean isUriExactMatch(String testPath, String requestPath) { //cant be an exact match if this is a regex if (isTestPathRegex(testPath)) { return false; } boolean retval = false; /** * Case 1: Exact Match * */ if (testPath.equals(requestPath)) { retval = true; } return retval; } /** * if there are methods specified, then they (e.g. GET) are unprotected, and * all others are protected * * @return the unprotected HTTP methods */ public Set<String> getUnprotectedMethods() { return config().getUnprotectedMethods(); } }