getUnprotectedMethods() {
+ return config().getUnprotectedMethods();
+ }
- return isValid;
+ @Override
+ public String toString() {
+ return isEnabled() ? new CsrfGuardPropertiesToStringBuilder(config()).toString()
+ : "OWASP CSRFGuard is disabled.";
+ }
+
+ /**
+ * Rotation in case of AJAX requests is not currently not supported because of the possible race conditions.
+ *
+ * A Single Page Application can fire multiple simultaneous requests.
+ * If rotation is enabled, the first request might trigger a token change before the validation of the second request with the same token, causing
+ * false-positive CSRF intrusion exceptions.
+ *
+ * @param request the current request
+ * @return True if rotation is enabled and possible
+ */
+ public boolean isRotateEnabled(final HttpServletRequest request) {
+ return isRotateEnabled() && !CsrfGuardUtils.isAjaxRequest(request);
}
/**
@@ -276,6 +297,7 @@ public void onSessionCreated(final LogicalSession logicalSession) {
if (isTokenPerPageEnabled()
&& isTokenPerPagePrecreate()
+ && isProtectEnabled()
&& !logicalSession.areTokensGenerated()) {
tokenService.generateProtectedPageTokens(logicalSessionKey);
@@ -323,7 +345,7 @@ public void writeLandingPage(final HttpServletRequest request, final HttpServlet
.append("\");");
/* only include token if needed */
- if (isProtectedPage(landingPage)) {
+ if (new CsrfValidator().isProtectedPage(landingPage).isProtected()) {
stringBuilder.append("var hiddenField = document.createElement(\"input\");")
.append("hiddenField.setAttribute(\"type\", \"hidden\");")
.append("hiddenField.setAttribute(\"name\", \"")
@@ -351,147 +373,6 @@ public void writeLandingPage(final HttpServletRequest request, final HttpServlet
response.getWriter().write(code);
}
- public boolean isProtectedPageAndMethod(final String page, final String method) {
- return (isProtectedPage(CsrfGuardUtils.normalizeResourceURI(page)) && isProtectedMethod(method));
- }
-
- public boolean isProtectedPageAndMethod(final HttpServletRequest request) {
- return isProtectedPageAndMethod(request.getRequestURI(), request.getMethod());
- }
-
- public boolean isPrintConfig() {
- return config().isPrintConfig();
- }
-
- public String getDomainOrigin() {
- return config().getDomainOrigin();
- }
-
- public Duration getPageTokenSynchronizationTolerance() {
- return config().getPageTokenSynchronizationTolerance();
- }
-
- /**
- * if there are methods specified, then they (e.g. GET) are unprotected, and all others are protected
- *
- * @return the unprotected HTTP methods
- */
- public Set getUnprotectedMethods() {
- return config().getUnprotectedMethods();
- }
-
- @Override
- public String toString() {
- return isEnabled() ? new CsrfGuardPropertiesToStringBuilder(config()).toString()
- : "OWASP CSRFGuard is disabled.";
- }
-
- private static boolean isUriPathMatch(final String testPath, final String requestPath) {
- return testPath.equals("/*") || (testPath.endsWith("/*")
- && (testPath.regionMatches(0, requestPath, 0, testPath.length() - 2)
- && (requestPath.length() == (testPath.length() - 2) || '/' == requestPath.charAt(testPath.length() - 2))));
- }
-
- private boolean isTokenValidInRequest(final HttpServletRequest request, final HttpServletResponse response) {
- boolean isValid = false;
-
- final CsrfGuard csrfGuard = CsrfGuard.getInstance();
- final LogicalSession logicalSession = csrfGuard.getLogicalSessionExtractor().extract(request);
-
- if (Objects.nonNull(logicalSession)) {
- final TokenService tokenService = getTokenService();
- final String logicalSessionKey = logicalSession.getKey();
- final String masterToken = tokenService.getMasterToken(logicalSessionKey);
-
- if (Objects.nonNull(masterToken)) {
- try {
- final TokenBO tokenBO = tokenService.verifyToken(request, logicalSessionKey, masterToken);
-
- final TokenTO tokenTO = isRotateEnabled(request) ? tokenService.rotateUsedToken(logicalSessionKey, request.getRequestURI(), tokenBO)
- : TokenMapper.toTransferObject(tokenBO);
-
- CsrfGuardUtils.addResponseTokenHeader(csrfGuard, request, response, tokenTO);
-
- isValid = true;
- } catch (final CsrfGuardException csrfe) {
- callActionsOnError(request, response, csrfe);
- }
- } else {
- callActionsOnError(request, response, new CsrfGuardException(MessageConstants.TOKEN_MISSING_FROM_STORAGE_MSG));
- }
- } else {
- callActionsOnError(request, response, new CsrfGuardException(MessageConstants.TOKEN_MISSING_FROM_STORAGE_MSG));
- }
-
- return isValid;
- }
-
- /**
- * Rotation in case of AJAX requests is not currently not supported because of the possible race conditions.
- *
- * A Single Page Application can fire multiple simultaneous requests.
- * If rotation is enabled, the first request might trigger a token change before the validation of the second request with the same token, causing
- * false-positive CSRF intrusion exceptions.
- *
- * @param request the current request
- * @return True if rotation is enabled and possible
- */
- private boolean isRotateEnabled(final HttpServletRequest request) {
- return isRotateEnabled() && !CsrfGuardUtils.isAjaxRequest(request);
- }
-
- private boolean isProtectedPage(final String normalizedResourceUri) {
- /* if this is a javascript page, let it go through */
- if (JavaScriptServlet.getJavascriptUris().contains(normalizedResourceUri)) {
- return false;
- }
-
- final Predicate predicate = page -> isUriMatch(page, normalizedResourceUri);
- return isProtectEnabled() ? getProtectedPages().stream().anyMatch(predicate) /* all links are unprotected, except the ones that were explicitly specified */
- : getUnprotectedPages().stream().noneMatch(predicate); /* all links are protected, except the ones were explicitly excluded */
- }
-
- /**
- * Whether or not the HTTP method is protected, i.e. should be checked for token.
- *
- * @param method The method to check for protection status
- * @return true when the given method name is in the protected methods set and not in the unprotected methods set
- */
- private boolean isProtectedMethod(final String method) {
- boolean isProtected = true;
-
- final Set protectedMethods = getProtectedMethods();
- if (!protectedMethods.isEmpty() && !protectedMethods.contains(method)) {
- isProtected = false;
- }
-
- final Set unprotectedMethods = getUnprotectedMethods();
- if (!unprotectedMethods.isEmpty() && unprotectedMethods.contains(method)) {
- isProtected = false;
- }
-
- return isProtected;
- }
-
- /**
- * FIXME: partially taken from Tomcat - ApplicationFilterFactory#matchFiltersURL
- *
- * @param testPath the pattern to match.
- * @param requestPath the current request path.
- * @return {@code true} if {@code requestPath} matches {@code testPath}.
- */
- private boolean isUriMatch(final String testPath, final String requestPath) {
- return Objects.nonNull(testPath) && (testPath.equals(requestPath)
- || isUriPathMatch(testPath, requestPath)
- || CsrfGuardUtils.isExtensionMatch(testPath, requestPath)
- || isUriRegexMatch(testPath, requestPath));
- }
-
- private boolean isUriRegexMatch(final String testPath, final String requestPath) {
- return RegexValidationUtil.isTestPathRegex(testPath) && this.regexPatternCache.computeIfAbsent(testPath, k -> Pattern.compile(testPath))
- .matcher(requestPath)
- .matches();
- }
private ConfigurationProvider config() {
if (this.properties == null) {
@@ -530,25 +411,6 @@ private ConfigurationProvider retrieveNewConfig() {
return configurationProvider;
}
- /**
- * Invoked when there was a CsrfGuardException such as a token mismatch error.
- * Calls the configured actions.
- *
- * @param request The HttpServletRequest
- * @param response The HttpServletResponse
- * @param csrfe The exception that triggered the actions call. Passed to the action.
- * @see IAction#execute(HttpServletRequest, HttpServletResponse, CsrfGuardException, CsrfGuard)
- */
- private void callActionsOnError(final HttpServletRequest request, final HttpServletResponse response, final CsrfGuardException csrfe) {
- for (final IAction action : getActions()) {
- try {
- action.execute(request, response, csrfe, this);
- } catch (final CsrfGuardException exception) {
- getLogger().log(LogLevel.Error, exception);
- }
- }
- }
-
private static final class SingletonHolder {
public static final CsrfGuard instance = new CsrfGuard();
}
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/CsrfGuardFilter.java b/csrfguard/src/main/java/org/owasp/csrfguard/CsrfGuardFilter.java
index 20d9489..125e5b1 100644
--- a/csrfguard/src/main/java/org/owasp/csrfguard/CsrfGuardFilter.java
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/CsrfGuardFilter.java
@@ -48,7 +48,7 @@ public final class CsrfGuardFilter implements Filter {
private FilterConfig filterConfig = null;
@Override
- public void init(final FilterConfig filterConfig) throws ServletException {
+ public void init(final FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
@@ -92,12 +92,13 @@ private void handleSession(final HttpServletRequest httpServletRequest, final In
if (logicalSession.isNew() && csrfGuard.isUseNewTokenLandingPage()) {
csrfGuard.writeLandingPage(httpServletRequest, interceptRedirectResponse, logicalSessionKey);
- } else if (csrfGuard.isValidRequest(httpServletRequest, interceptRedirectResponse)) {
+ } else if (new CsrfValidator().isValid(httpServletRequest, interceptRedirectResponse)) {
filterChain.doFilter(httpServletRequest, interceptRedirectResponse);
} else {
logInvalidRequest(httpServletRequest, csrfGuard);
}
+ // TODO this is not needed in case of un-protected pages
final String requestURI = httpServletRequest.getRequestURI();
final String generatedToken = csrfGuard.getTokenService().generateTokensIfAbsent(logicalSessionKey, httpServletRequest.getMethod(), requestURI);
@@ -107,7 +108,7 @@ private void handleSession(final HttpServletRequest httpServletRequest, final In
private void handleNoSession(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final InterceptRedirectResponse interceptRedirectResponse, final FilterChain filterChain,
final CsrfGuard csrfGuard) throws IOException, ServletException {
if (csrfGuard.isValidateWhenNoSessionExists()) {
- if (csrfGuard.isValidRequest(httpServletRequest, interceptRedirectResponse)) {
+ if (new CsrfValidator().isValid(httpServletRequest, interceptRedirectResponse)) {
filterChain.doFilter(httpServletRequest, interceptRedirectResponse);
} else {
logInvalidRequest(httpServletRequest, csrfGuard);
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/CsrfValidator.java b/csrfguard/src/main/java/org/owasp/csrfguard/CsrfValidator.java
new file mode 100644
index 0000000..9b2208d
--- /dev/null
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/CsrfValidator.java
@@ -0,0 +1,247 @@
+/*
+ * The OWASP CSRFGuard Project, BSD License
+ * Copyright (c) 2011, Eric Sheridan (eric@infraredsecurity.com)
+ * 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 org.apache.commons.lang3.StringUtils;
+import org.owasp.csrfguard.action.IAction;
+import org.owasp.csrfguard.log.ILogger;
+import org.owasp.csrfguard.log.LogLevel;
+import org.owasp.csrfguard.servlet.JavaScriptServlet;
+import org.owasp.csrfguard.session.LogicalSession;
+import org.owasp.csrfguard.token.businessobject.TokenBO;
+import org.owasp.csrfguard.token.mapper.TokenMapper;
+import org.owasp.csrfguard.token.service.TokenService;
+import org.owasp.csrfguard.token.transferobject.TokenTO;
+import org.owasp.csrfguard.util.CsrfGuardUtils;
+import org.owasp.csrfguard.util.MessageConstants;
+import org.owasp.csrfguard.util.RegexValidationUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.UnaryOperator;
+import java.util.regex.Pattern;
+
+public final class CsrfValidator {
+
+ private final CsrfGuard csrfGuard;
+
+ public CsrfValidator() {
+ this.csrfGuard = CsrfGuard.getInstance();
+ }
+
+ public boolean isValid(final HttpServletRequest request, final HttpServletResponse response) {
+ final boolean isValid;
+
+ final ILogger logger = this.csrfGuard.getLogger();
+ final String normalizedResourceURI = CsrfGuardUtils.normalizeResourceURI(request);
+ final ProtectionResult protectionResult = isProtectedPageAndMethod(request);
+ if (protectionResult.isProtected()) {
+ logger.log(LogLevel.Debug, String.format("CSRFGuard analyzing protected resource: '%s'", normalizedResourceURI));
+ isValid = isTokenValidInRequest(request, response, protectionResult.getResourceIdentifier());
+ } else {
+ logger.log(LogLevel.Debug, String.format("Unprotected page: %s", normalizedResourceURI));
+ isValid = true;
+ }
+
+ return isValid;
+ }
+
+ public ProtectionResult isProtectedPageAndMethod(final String page, final String method) {
+ final String normalizedResourceUri = CsrfGuardUtils.normalizeResourceURI(page);
+ final ProtectionResult protectionResult = isProtectedPage(normalizedResourceUri);
+
+ return (protectionResult.isProtected() && isProtectedMethod(method)) ? protectionResult
+ : new ProtectionResult(false, normalizedResourceUri);
+ }
+
+ ProtectionResult isProtectedPage(final String normalizedResourceUri) {
+ final ProtectionResult protectionResult;
+
+ if (JavaScriptServlet.getJavascriptUris().contains(normalizedResourceUri)) {
+ /* if this is a javascript page, let it go through */
+ protectionResult = new ProtectionResult(false, normalizedResourceUri);
+ } else if (this.csrfGuard.isProtectEnabled()) {
+ /* all links are unprotected, except the ones that were explicitly specified */
+ protectionResult = isUriMatch(normalizedResourceUri, this.csrfGuard.getProtectedPages(), v -> v, false);
+ } else {
+ /* all links are protected, except the ones were explicitly excluded */
+ protectionResult = isUriMatch(normalizedResourceUri, this.csrfGuard.getUnprotectedPages(), v -> new ProtectionResult(false, v.getResourceIdentifier()), true);
+ }
+ return protectionResult;
+ }
+
+ private static boolean isUriPathMatch(final String configuredPageUri, final String requestUri) {
+ return configuredPageUri.equals("/*") || (configuredPageUri.endsWith("/*") && (configuredPageUri.regionMatches(0, requestUri, 0, configuredPageUri.length() - 2)
+ && ((requestUri.length() == (configuredPageUri.length() - 2)) || ('/' == requestUri.charAt(configuredPageUri.length() - 2)))));
+ }
+
+ /**
+ * FIXME: taken from Tomcat - ApplicationFilterFactory#matchFiltersURL
+ */
+ private static boolean isExtensionMatch(final String testPath, final String requestPath) {
+ final boolean result;
+ if (StringUtils.startsWith(testPath, "*.")) {
+ final int slash = requestPath.lastIndexOf('/');
+ final int period = requestPath.lastIndexOf('.');
+
+ if ((slash >= 0)
+ && (period > slash)
+ && (period != requestPath.length() - 1)
+ && ((requestPath.length() - period) == (testPath.length() - 1))) {
+ result = testPath.regionMatches(2, requestPath, period + 1, testPath.length() - 2);
+ } else {
+ result = false;
+ }
+ } else {
+ result = false;
+ }
+
+ return result;
+ }
+
+ private ProtectionResult isUriMatch(final String normalizedResourceUri, final Set pages, final UnaryOperator operator, final boolean isProtected) {
+ for (final String page : pages) {
+ final ProtectionResult protectionResult = isUriMatch(page, normalizedResourceUri);
+ if (protectionResult.isProtected()) {
+ return operator.apply(protectionResult);
+ }
+ }
+ return new ProtectionResult(isProtected, normalizedResourceUri);
+ }
+
+ private TokenService getTokenService() {
+ return new TokenService(this.csrfGuard);
+ }
+
+ private ProtectionResult isProtectedPageAndMethod(final HttpServletRequest request) {
+ return isProtectedPageAndMethod(request.getRequestURI(), request.getMethod());
+ }
+
+ /**
+ * Whether or not the HTTP method is protected, i.e. should be checked for token.
+ *
+ * @param method The method to check for protection status
+ * @return true when the given method name is in the protected methods set and not in the unprotected methods set
+ */
+ private boolean isProtectedMethod(final String method) {
+ boolean isProtected = true;
+
+ final Set protectedMethods = this.csrfGuard.getProtectedMethods();
+ if (!protectedMethods.isEmpty() && !protectedMethods.contains(method)) {
+ isProtected = false;
+ }
+
+ final Set unprotectedMethods = this.csrfGuard.getUnprotectedMethods();
+ if (!unprotectedMethods.isEmpty() && unprotectedMethods.contains(method)) {
+ isProtected = false;
+ }
+
+ return isProtected;
+ }
+
+ /**
+ * @param configuredPageUri the pattern to match.
+ * @param requestUri the current request path.
+ * @return {@code true} if {@code requestUri} matches {@code configuredPageUri}.
+ */
+ private ProtectionResult isUriMatch(final String configuredPageUri, final String requestUri) {
+ if (Objects.nonNull(configuredPageUri)) {
+ if (configuredPageUri.equals(requestUri) || isUriPathMatch(configuredPageUri, requestUri) || isExtensionMatch(configuredPageUri, requestUri)) {
+ return new ProtectionResult(true, requestUri);
+ } else if (isUriRegexMatch(configuredPageUri, requestUri)) {
+ return new ProtectionResult(true, configuredPageUri);
+ } else {
+ return new ProtectionResult(false, requestUri);
+ }
+ } else {
+ return new ProtectionResult(false, requestUri);
+ }
+ }
+
+ private boolean isUriRegexMatch(final String configuredPageUri, final String requestUri) {
+ return RegexValidationUtil.isTestPathRegex(configuredPageUri) && this.csrfGuard.getRegexPatternCache().computeIfAbsent(configuredPageUri, k -> Pattern.compile(configuredPageUri))
+ .matcher(requestUri)
+ .matches();
+ }
+
+ private boolean isTokenValidInRequest(final HttpServletRequest request, final HttpServletResponse response, final String resourceIdentifier) {
+ boolean isValid = false;
+
+ final CsrfGuard csrfGuard = CsrfGuard.getInstance();
+ final LogicalSession logicalSession = csrfGuard.getLogicalSessionExtractor().extract(request);
+
+ if (Objects.nonNull(logicalSession)) {
+ final TokenService tokenService = getTokenService();
+ final String logicalSessionKey = logicalSession.getKey();
+ final String masterToken = tokenService.getMasterToken(logicalSessionKey);
+
+ if (Objects.nonNull(masterToken)) {
+ try {
+ final TokenBO tokenBO = tokenService.verifyToken(request, resourceIdentifier, logicalSessionKey, masterToken);
+
+ final TokenTO tokenTO = csrfGuard.isRotateEnabled(request) ? tokenService.rotateUsedToken(logicalSessionKey, resourceIdentifier, tokenBO)
+ : TokenMapper.toTransferObject(tokenBO);
+
+ CsrfGuardUtils.addResponseTokenHeader(csrfGuard, request, response, tokenTO);
+
+ isValid = true;
+ } catch (final CsrfGuardException e) {
+ callActionsOnError(request, response, e);
+ }
+ } else {
+ callActionsOnError(request, response, new CsrfGuardException(MessageConstants.TOKEN_MISSING_FROM_STORAGE_MSG));
+ }
+ } else {
+ callActionsOnError(request, response, new CsrfGuardException(MessageConstants.TOKEN_MISSING_FROM_STORAGE_MSG));
+ }
+
+ return isValid;
+ }
+
+ /**
+ * Invoked when there was a CsrfGuardException such as a token mismatch error.
+ * Calls the configured actions.
+ *
+ * @param request The HttpServletRequest
+ * @param response The HttpServletResponse
+ * @param csrfGuardException The exception that triggered the actions call. Passed to the action.
+ * @see IAction#execute(HttpServletRequest, HttpServletResponse, CsrfGuardException, CsrfGuard)
+ */
+ private void callActionsOnError(final HttpServletRequest request, final HttpServletResponse response, final CsrfGuardException csrfGuardException) {
+ for (final IAction action : this.csrfGuard.getActions()) {
+ try {
+ action.execute(request, response, csrfGuardException, this.csrfGuard);
+ } catch (final CsrfGuardException exception) {
+ this.csrfGuard.getLogger().log(LogLevel.Error, exception);
+ }
+ }
+ }
+}
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/ProtectionResult.java b/csrfguard/src/main/java/org/owasp/csrfguard/ProtectionResult.java
new file mode 100644
index 0000000..47ca270
--- /dev/null
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/ProtectionResult.java
@@ -0,0 +1,48 @@
+/*
+ * The OWASP CSRFGuard Project, BSD License
+ * Copyright (c) 2011, Eric Sheridan (eric@infraredsecurity.com)
+ * 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;
+
+public class ProtectionResult {
+
+ private final boolean isProtected;
+ private final String resourceIdentifier;
+
+ public ProtectionResult(final boolean isProtected, final String resourceIdentifier) {
+ this.isProtected = isProtected;
+ this.resourceIdentifier = resourceIdentifier;
+ }
+
+ public boolean isProtected() {
+ return this.isProtected;
+ }
+
+ public String getResourceIdentifier() {
+ return this.resourceIdentifier;
+ }
+}
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/config/PropertiesConfigurationProvider.java b/csrfguard/src/main/java/org/owasp/csrfguard/config/PropertiesConfigurationProvider.java
index 2a70469..4d70171 100644
--- a/csrfguard/src/main/java/org/owasp/csrfguard/config/PropertiesConfigurationProvider.java
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/config/PropertiesConfigurationProvider.java
@@ -43,6 +43,7 @@
import org.owasp.csrfguard.token.storage.LogicalSessionExtractor;
import org.owasp.csrfguard.token.storage.TokenHolder;
import org.owasp.csrfguard.util.CsrfGuardUtils;
+import org.owasp.csrfguard.util.RegexValidationUtil;
import javax.servlet.ServletConfig;
import java.io.IOException;
@@ -507,19 +508,36 @@ private static Pair getPropertyDirective(final String propertyK
return result;
}
- private static String getPageProperty(final Properties properties, final String propertyKey, final String propertyKeyPrefix) {
+ private String getPageProperty(final Properties properties, final String propertyKey, final String propertyKeyPrefix) {
String result = null;
final String pageProperty = getPrimaryPropertyDirective(propertyKey, propertyKeyPrefix);
if (Objects.nonNull(pageProperty)) {
final String pageUri = PropertyUtils.getProperty(properties, propertyKey);
- result = CsrfGuardUtils.normalizeResourceURI(pageUri);
+ result = isSpecialUriDescriptor(pageUri) ? pageUri
+ : CsrfGuardUtils.normalizeResourceURI(pageUri);
}
return result;
}
+ /**
+ * Decides whether the given resourceUri is a static descriptor or a matching rule
+ * Has to be in sync with org.owasp.csrfguard.CsrfValidator.isUriMatch(java.lang.String, java.lang.String) and calculatePageTokenForUri in the JS logic
+ */
+ private boolean isSpecialUriDescriptor(final String resourceUri) {
+ if (this.tokenPerPage && (resourceUri.endsWith("/*") || resourceUri.startsWith("*."))) {
+ // FIXME implement in the JS logic (calculatePageTokenForUri)
+ this.logger.log(LogLevel.Warning, "'Extension' and 'partial path wildcard' matching for page tokens is not supported properly yet! " +
+ "Every resource will be assigned a new unique token instead of using the defined resource matcher token. " +
+ "Although this is not a security issue, in case of a large REST application it can have an impact on performance." +
+ "Consider using regular expressions instead.");
+ }
+
+ return RegexValidationUtil.isTestPathRegex(resourceUri) || resourceUri.startsWith("/*") || resourceUri.endsWith("/*") || resourceUri.startsWith("*.");
+ }
+
private void javascriptInitParamsIfNeeded() {
if (!this.javascriptParamsInitialized) {
final ServletConfig servletConfig = JavaScriptServlet.getStaticServletConfig();
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/http/InterceptRedirectResponse.java b/csrfguard/src/main/java/org/owasp/csrfguard/http/InterceptRedirectResponse.java
index 1fd89b9..be04200 100644
--- a/csrfguard/src/main/java/org/owasp/csrfguard/http/InterceptRedirectResponse.java
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/http/InterceptRedirectResponse.java
@@ -30,6 +30,8 @@
package org.owasp.csrfguard.http;
import org.owasp.csrfguard.CsrfGuard;
+import org.owasp.csrfguard.CsrfValidator;
+import org.owasp.csrfguard.ProtectionResult;
import org.owasp.csrfguard.session.LogicalSession;
import org.owasp.csrfguard.token.service.TokenService;
@@ -60,7 +62,8 @@ public void sendRedirect(final String location) throws IOException {
final String sanitizedLocation = location.replaceAll("(\\r|\\n|%0D|%0A|%0a|%0d)", "");
/* ensure token included in redirects */
- if (!sanitizedLocation.contains("://") && this.csrfGuard.isProtectedPageAndMethod(sanitizedLocation, "GET")) {
+ final ProtectionResult protectionResult = new CsrfValidator().isProtectedPageAndMethod(sanitizedLocation, "GET");
+ if (!sanitizedLocation.contains("://") && protectionResult.isProtected()) {
// Separate URL fragment from path, e.g. /myPath#myFragment becomes [0]: /myPath [1]: myFragment
final String[] splitOnFragment = location.split("#", 2);
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/servlet/JavaScriptServlet.java b/csrfguard/src/main/java/org/owasp/csrfguard/servlet/JavaScriptServlet.java
index 1445be2..ce5f46b 100644
--- a/csrfguard/src/main/java/org/owasp/csrfguard/servlet/JavaScriptServlet.java
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/servlet/JavaScriptServlet.java
@@ -33,6 +33,7 @@
import org.apache.commons.lang3.StringUtils;
import org.owasp.csrfguard.CsrfGuard;
import org.owasp.csrfguard.CsrfGuardServletContextListener;
+import org.owasp.csrfguard.CsrfValidator;
import org.owasp.csrfguard.log.LogLevel;
import org.owasp.csrfguard.session.LogicalSession;
import org.owasp.csrfguard.token.storage.LogicalSessionExtractor;
@@ -142,7 +143,7 @@ public void doGet(final HttpServletRequest request, final HttpServletResponse re
public void doPost(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
final CsrfGuard csrfGuard = CsrfGuard.getInstance();
- if (csrfGuard.isValidRequest(request, response)) {
+ if (new CsrfValidator().isValid(request, response)) {
if (csrfGuard.isTokenPerPageEnabled()) {
// TODO pass the logical session downstream, see whether the null check can be done from here
final LogicalSession logicalSession = csrfGuard.getLogicalSessionExtractor().extract(request);
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/token/service/TokenService.java b/csrfguard/src/main/java/org/owasp/csrfguard/token/service/TokenService.java
index ec6ff03..9bac95c 100644
--- a/csrfguard/src/main/java/org/owasp/csrfguard/token/service/TokenService.java
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/token/service/TokenService.java
@@ -30,6 +30,8 @@
import org.owasp.csrfguard.CsrfGuard;
import org.owasp.csrfguard.CsrfGuardException;
+import org.owasp.csrfguard.CsrfValidator;
+import org.owasp.csrfguard.ProtectionResult;
import org.owasp.csrfguard.session.LogicalSession;
import org.owasp.csrfguard.token.TokenUtils;
import org.owasp.csrfguard.token.businessobject.TokenBO;
@@ -115,8 +117,9 @@ public String generateTokensIfAbsent(final String logicalSessionKey, final Strin
final TokenHolder tokenHolder = this.csrfGuard.getTokenHolder();
if (this.csrfGuard.isTokenPerPageEnabled()) {
- if (this.csrfGuard.isProtectedPageAndMethod(requestURI, httpMethod)) {
- return tokenHolder.createPageTokenIfAbsent(logicalSessionKey, CsrfGuardUtils.normalizeResourceURI(requestURI), TokenUtils::generateRandomToken);
+ final ProtectionResult protectionResult = new CsrfValidator().isProtectedPageAndMethod(requestURI, httpMethod);
+ if (protectionResult.isProtected()) {
+ return tokenHolder.createPageTokenIfAbsent(logicalSessionKey, protectionResult.getResourceIdentifier(), TokenUtils::generateRandomToken);
}
}
@@ -161,7 +164,6 @@ public void generateProtectedPageTokens(final String logicalSessionKey) {
* @return a TokenTO transfer object containing the updated token values that will be sent back to the client
*/
public TokenTO rotateUsedToken(final String logicalSessionKey, final String requestURI, final TokenBO usedValidToken) {
- final String normalizedResourceURI = CsrfGuardUtils.normalizeResourceURI(requestURI);
final TokenHolder tokenHolder = this.csrfGuard.getTokenHolder();
final String newTokenValue = TokenUtils.generateRandomToken();
@@ -170,8 +172,8 @@ public TokenTO rotateUsedToken(final String logicalSessionKey, final String requ
tokenHolder.setMasterToken(logicalSessionKey, newTokenValue);
usedValidToken.setUpdatedMasterToken(newTokenValue);
} else {
- tokenHolder.setPageToken(logicalSessionKey, normalizedResourceURI, newTokenValue);
- usedValidToken.setUpdatedPageToken(normalizedResourceURI, newTokenValue);
+ tokenHolder.setPageToken(logicalSessionKey, requestURI, newTokenValue);
+ usedValidToken.setUpdatedPageToken(requestURI, newTokenValue);
}
return TokenMapper.toTransferObject(usedValidToken);
@@ -214,12 +216,13 @@ public String getTokenValue(final String logicalSessionKey, final String resourc
*
*
* @param request current HTTP Servlet Request
+ * @param resourceIdentifier the requested resource identifier
* @param logicalSessionKey identifies the current logical session uniquely
* @param masterToken the master token
* @return The TokenBO business object that contains the updated tokens and the token used to validate the current request
* @throws CsrfGuardException if the request does not have a valid token associated
*/
- public TokenBO verifyToken(final HttpServletRequest request, final String logicalSessionKey, final String masterToken) throws CsrfGuardException {
+ public TokenBO verifyToken(final HttpServletRequest request, final String resourceIdentifier, final String logicalSessionKey, final String masterToken) throws CsrfGuardException {
final String tokenName = this.csrfGuard.getTokenName();
final boolean isAjaxRequest = this.csrfGuard.isAjaxEnabled() && CsrfGuardUtils.isAjaxRequest(request);
@@ -228,7 +231,7 @@ public TokenBO verifyToken(final HttpServletRequest request, final String logica
if (Objects.isNull(tokenFromRequest)) {
throw new CsrfGuardException(MessageConstants.REQUEST_MISSING_TOKEN_MSG);
} else {
- tokenBO = this.csrfGuard.isTokenPerPageEnabled() ? verifyPageToken(logicalSessionKey, masterToken, tokenFromRequest, CsrfGuardUtils.normalizeResourceURI(request), isAjaxRequest)
+ tokenBO = this.csrfGuard.isTokenPerPageEnabled() ? verifyPageToken(logicalSessionKey, masterToken, tokenFromRequest, resourceIdentifier, isAjaxRequest)
: verifyMasterToken(masterToken, tokenFromRequest);
}
diff --git a/csrfguard/src/main/java/org/owasp/csrfguard/util/CsrfGuardUtils.java b/csrfguard/src/main/java/org/owasp/csrfguard/util/CsrfGuardUtils.java
index 1684ef6..0f702c1 100644
--- a/csrfguard/src/main/java/org/owasp/csrfguard/util/CsrfGuardUtils.java
+++ b/csrfguard/src/main/java/org/owasp/csrfguard/util/CsrfGuardUtils.java
@@ -113,30 +113,6 @@ public static T newInstance(final Class theClass) {
return ConfigPropertiesCascadeCommonUtils.newInstance(theClass);
}
- /**
- * FIXME: taken from Tomcat - ApplicationFilterFactory#matchFiltersURL
- */
- public static boolean isExtensionMatch(final String testPath, final String requestPath) {
- final boolean result;
- if (StringUtils.startsWith(testPath, "*.")) {
- final int slash = requestPath.lastIndexOf('/');
- final int period = requestPath.lastIndexOf('.');
-
- if ((slash >= 0)
- && (period > slash)
- && (period != requestPath.length() - 1)
- && ((requestPath.length() - period) == (testPath.length() - 1))) {
- result = testPath.regionMatches(2, requestPath, period + 1, testPath.length() - 2);
- } else {
- result = false;
- }
- } else {
- result = false;
- }
-
- return result;
- }
-
public static void addResponseTokenHeader(final CsrfGuard csrfGuard, final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final TokenTO tokenTO) {
if (csrfGuard.isAjaxEnabled() && CsrfGuardUtils.isAjaxRequest(httpServletRequest)) {
if (!tokenTO.isEmpty()) {
diff --git a/csrfguard/src/main/resources/csrfguard.js b/csrfguard/src/main/resources/csrfguard.js
index 9f7a370..06f2899 100644
--- a/csrfguard/src/main/resources/csrfguard.js
+++ b/csrfguard/src/main/resources/csrfguard.js
@@ -311,6 +311,30 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
return uri;
}
+ function calculatePageTokenForUri(pageTokens, uri) {
+ let value = null;
+ Object.keys(pageTokens).forEach(function (pageTokenKey) {
+ var pageToken = pageTokens[pageTokenKey];
+
+ if (uri === pageTokenKey) {
+ value = pageToken;
+ } else if (pageTokenKey.startsWith('^') && pageTokenKey.endsWith('$')) { // regex matching
+ if (new RegExp(pageTokenKey).test(uri)) {
+ value = pageToken;
+ }
+ } else if (pageTokenKey.startsWith('/*')) { // full path wildcard path matching
+ value = pageToken;
+ } else if (pageTokenKey.endsWith('/*') || pageTokenKey.startsWith('.*')) { // 'partial path wildcard' and 'extension' matching
+ // TODO implement
+ console.warn("'Extension' and 'partial path wildcard' matching for page tokens is not supported properly yet! " +
+ "Every resource will be assigned a new unique token instead of using the defined resource matcher token. " +
+ "Although this is not a security issue, in case of a large REST application it can have an impact on performance." +
+ "Consider using regular expressions instead.");
+ }
+ });
+ return value;
+ }
+
/**
* inject tokens as hidden fields into forms
*/
@@ -327,13 +351,14 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
var value = tokenValue;
var action = form.getAttribute('action');
- if (isValidUrl(action)) {
- if (action !== null) {
- var uri = parseUri(action);
- value = pageTokens[uri] != null ? pageTokens[uri] : tokenValue;
- }
+ if (action !== null && isValidUrl(action)) {
+ var uri = parseUri(action);
+ const calculatedPageToken = calculatePageTokenForUri(pageTokens, uri);
+ value = calculatedPageToken == null ? tokenValue : calculatedPageToken;
- let hiddenTokenFields = Object.keys(form.elements).filter(i => form.elements[i].name === tokenName); // TODO do not use arrow functions because IE does not support it
+ let hiddenTokenFields = Object.keys(form.elements).filter(function (i) {
+ return form.elements[i].name === tokenName;
+ });
if (hiddenTokenFields.length === 0) {
var hidden = document.createElement('input');
@@ -345,7 +370,9 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
form.appendChild(hidden);
console.debug('Hidden input element [', hidden, '] was added to the form: ', form);
} else {
- hiddenTokenFields.forEach(i => form.elements[i].value = value); // TODO do not use arrow functions because IE does not support it
+ hiddenTokenFields.forEach(function (i) {
+ return form.elements[i].value = value;
+ });
console.debug('Hidden token fields [', hiddenTokenFields, '] of form [', form, '] were updated with new token value: ', value);
}
}
@@ -370,7 +397,8 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
if (location != null && isValidUrl(location) && !isUnprotectedExtension(location)) {
const uri = parseUri(location);
- const value = (pageTokens[uri] != null ? pageTokens[uri] : tokenValue);
+ const calculatedPageToken = calculatePageTokenForUri(pageTokens, uri);
+ const value = calculatedPageToken == null ? tokenValue : calculatedPageToken;
const tokenValueMatcher = new RegExp('(?:' + tokenName + '=)([^?|#|&]+)', 'gi');
const tokenMatches = tokenValueMatcher.exec(location);
@@ -396,7 +424,9 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
}
} else {
let newLocation = location;
- tokenMatches.slice(1).forEach(match => newLocation = newLocation.replace(match, value)); // TODO do not use arrow functions because IE does not support it
+ tokenMatches.slice(1).forEach(function (match) {
+ return newLocation = newLocation.replace(match, value);
+ });
element.setAttribute(attr, newLocation);
console.debug('Attribute [', attr, '] with value [', newLocation, '] set for element: ', element);
@@ -509,6 +539,7 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let pageTokens = JSON.parse(xhr.responseText)['pageTokens'];
+ console.debug('Received page tokens: ', pageTokens);
callback.call(this, pageTokens);
} else {
alert(xhr.status + ': CSRF check failed');
@@ -561,6 +592,7 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
if (isValidDomain(document.domain, '%DOMAIN_ORIGIN%')) {
var tokenName = '%TOKEN_NAME%';
var masterTokenValue = '%TOKEN_VALUE%';
+ console.debug('Master token [' + tokenName + ']: ', masterTokenValue);
var isLoadedWrapper = {isDomContentLoaded: false};
@@ -599,11 +631,15 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
let newMasterToken = tokenTO['masterToken'];
if (newMasterToken !== undefined) {
masterTokenValue = newMasterToken;
+ console.debug('New master token value received: ', masterTokenValue);
}
let newPageTokens = tokenTO['pageTokens'];
if (newPageTokens !== undefined) {
- Object.keys(newPageTokens).forEach(key => pageTokenWrapper.pageTokens[key] = newPageTokens[key]); // TODO do not use arrow functions because IE does not support it
+ Object.keys(newPageTokens).forEach(function (key) {
+ return pageTokenWrapper.pageTokens[key] = newPageTokens[key];
+ });
+ console.debug('New page token value(s) received: ', newPageTokens);
}
injectTokens(tokenName, masterTokenValue, pageTokenWrapper.pageTokens);
@@ -614,16 +650,16 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
}
});
- var computePageToken = function(modifiedUrl) {
+ var computePageToken = function(pageTokens, modifiedUri) {
let result = null;
let pathWithoutLeadingSlash = window.location.pathname.substring(1); // e.g. deploymentName/service/endpoint
let pathArray = pathWithoutLeadingSlash.split('/');
let builtPath = '';
- for (let i = 0; i < pathArray.length - 1; i++) { // the last part of the URL (endpoint) is disregarded because the modifiedUrl parameter is used instead
+ for (let i = 0; i < pathArray.length - 1; i++) { // the last part of the URI (endpoint) is disregarded because the modifiedUri parameter is used instead
builtPath += '/' + pathArray[i];
- let pageTokenValue = pageTokenWrapper.pageTokens[builtPath + modifiedUrl];
+ let pageTokenValue = calculatePageTokenForUri(pageTokens, builtPath + modifiedUri);
if (pageTokenValue != undefined) {
result = pageTokenValue;
break;
@@ -643,6 +679,10 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
return index > 0 ? currentUrl.substring(0, index) : currentUrl;
}
+ /*
+ * TODO should other checks be done here like in the isValidUrl?
+ * Could the url parameter contain full URLs with protocol domain, port etc?
+ */
let normalizedUrl = url.startsWith('/') ? url : '/' + url;
normalizedUrl = removeParameters(normalizedUrl, '?');
@@ -659,9 +699,9 @@ if (owaspCSRFGuardScriptHasLoaded !== true) {
if (pageTokenWrapper.pageTokens === null) {
this.setRequestHeader(tokenName, masterTokenValue);
} else {
- let pageToken = pageTokenWrapper.pageTokens[normalizedUrl];
+ let pageToken = calculatePageTokenForUri(pageTokenWrapper.pageTokens, normalizedUrl);
if (pageToken == undefined) {
- let computedPageToken = computePageToken(normalizedUrl);
+ let computedPageToken = computePageToken(pageTokenWrapper.pageTokens, normalizedUrl);
if (computedPageToken === null) {
this.setRequestHeader(tokenName, masterTokenValue);
diff --git a/csrfguard/src/test/java/org/owasp/csrfguard/CsrfValidatorTest.java b/csrfguard/src/test/java/org/owasp/csrfguard/CsrfValidatorTest.java
new file mode 100644
index 0000000..1e40ff9
--- /dev/null
+++ b/csrfguard/src/test/java/org/owasp/csrfguard/CsrfValidatorTest.java
@@ -0,0 +1,180 @@
+/*
+ * The OWASP CSRFGuard Project, BSD License
+ * Copyright (c) 2011, Eric Sheridan (eric@infraredsecurity.com)
+ * 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 org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.owasp.csrfguard.servlet.JavaScriptServlet;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class CsrfValidatorTest {
+
+ @Test
+ void testResourceValidationWithNoRules() {
+ final Set matchingRules = Collections.emptySet();
+ final List resourcesToTest = Arrays.asList("test.html", "/test.html");
+
+ testResourceValidation(matchingRules, Collections.emptyList(), resourcesToTest);
+ }
+
+ @Test
+ void testResourceValidationWithPathMatchingRule() {
+ final Set matchingRules = Collections.singleton("/*");
+
+ final List resourceSet = Arrays.asList("protect.html", "/protect.html", "something/protect.txt", "/something/protect.txt");
+
+ testResourceValidation(matchingRules, resourceSet, Collections.emptyList());
+ }
+
+ @Test
+ void testResourceValidationWithStaticRules() {
+ final Set matchingRules = Stream.of("/unprotected.html", "/also_unprotected.jsp").collect(Collectors.toSet());
+
+ final List firstResourceSet = Arrays.asList("unprotected.html", "also_unprotected.jsp", "/unprotected.html", "/also_unprotected.jsp");
+ final List secondResourceSet = Arrays.asList("test.html", "/test.html", "test.jsp", "/test.jsp");
+
+ testResourceValidation(matchingRules, firstResourceSet, secondResourceSet);
+ }
+
+ @Test
+ void testResourceValidationWithRegexRules() {
+ final String matchingRule = "^.*protect\\..*$";
+ final List firstResourceSet = Arrays.asList("protect.html", "/protect.html", "something/protect.txt", "/something/protect.txt");
+ final List secondResourceSet = Arrays.asList("test.html", "/test.html", "/protect");
+
+ testResourceValidation(matchingRule, firstResourceSet, secondResourceSet);
+ }
+
+ @Test
+ void testResourceValidationWithPartialPathMatchingRule() {
+ final String matchingRule = "/protected/*";
+
+ final List firstResourceSet = Arrays.asList("protected/test.html", "/protected/test.html");
+ final List secondResourceSet = Arrays.asList("test.html", "/test.html", "/protect");
+
+ testResourceValidation(matchingRule, firstResourceSet, secondResourceSet);
+ }
+
+ @Test
+ void testResourceValidationWithExtensionMatchingRule() {
+ final String matchingRule = "*.jsp";
+
+ final List firstResourceSet = Arrays.asList("protected/test.jsp", "/protected/test.jsp");
+ final List secondResourceSet = Arrays.asList("something/test.html", "/test.html", "/protect");
+
+ testResourceValidation(matchingRule, firstResourceSet, secondResourceSet);
+ }
+
+ private static void executeInMockedContext(final Consumer csrfGuardConsumer) {
+ final CsrfGuard csrfGuard = mock(CsrfGuard.class);
+
+ try (final MockedStatic javaScriptServletMockedStatic = mockStatic(JavaScriptServlet.class)) {
+ javaScriptServletMockedStatic.when(JavaScriptServlet::getJavascriptUris).thenReturn(Collections.singleton("/JavaScriptServlet"));
+
+ try (final MockedStatic csrfGuardMockedStatic = mockStatic(CsrfGuard.class)) {
+ csrfGuardMockedStatic.when(CsrfGuard::getInstance).thenReturn(csrfGuard);
+ csrfGuardConsumer.accept(CsrfGuard.getInstance());
+ }
+ }
+ }
+
+ private void testResourceValidation(final String matchingRule, final List firstResourceSet, final List secondResourceSet) {
+ testResourceValidation(Collections.singleton(matchingRule), firstResourceSet, secondResourceSet);
+ }
+
+ private void testResourceValidation(final Set matchingRule, final List firstResourceSet, final List secondResourceSet) {
+ testResourceValidation(true, matchingRule, firstResourceSet, secondResourceSet);
+ testResourceValidation(false, matchingRule, secondResourceSet, firstResourceSet);
+ }
+
+ private void testResourceValidation(final boolean isProtect, final Set matchingRules, final List protectedResources, final List unProtectedResources) {
+ executeInMockedContext(csrfGuard -> {
+ final CsrfValidator csrfValidator = initializeValidator(csrfGuard, isProtect, matchingRules);
+
+ assertPagesAreProtected(csrfValidator, protectedResources);
+ assertPagesAreNotProtected(csrfValidator, unProtectedResources);
+ });
+ }
+
+ private CsrfValidator initializeValidator(final CsrfGuard csrfGuard, final boolean isProtect, final Set matchingRules) {
+ when(csrfGuard.isProtectEnabled()).thenReturn(isProtect);
+
+ if (isProtect) {
+ when(csrfGuard.getProtectedPages()).thenReturn(matchingRules);
+ } else {
+ when(csrfGuard.getUnprotectedPages()).thenReturn(matchingRules);
+ }
+
+ return new CsrfValidator();
+ }
+
+ private void assertPagesAreProtected(final CsrfValidator csrfValidator, final List resources) {
+ assertPageProtection(csrfValidator, true, resources);
+ }
+
+ private void assertPagesAreNotProtected(final CsrfValidator csrfValidator, final List resources) {
+ assertPageProtection(csrfValidator, false, resources);
+ }
+
+ private void assertPageProtection(final CsrfValidator csrfValidator, final boolean isProtected, final List resources) {
+ resources.forEach(resource -> {
+ final ProtectionResult protectionResult = csrfValidator.isProtectedPageAndMethod(resource, "not relevant");
+
+ testResourceNormalized(resource, protectionResult);
+
+ if (isProtected != protectionResult.isProtected()) {
+ fail(String.format("The '%s' resource should %s protected!", resource, isProtected ? "be" : "not be"));
+ }
+ });
+ }
+
+ private void testResourceNormalized(final String resource, final ProtectionResult protectionResult) {
+ final String normalizedResource = protectionResult.getResourceIdentifier();
+ final Predicate normalizationTester = startingCharacter -> (normalizedResource.charAt(0) != startingCharacter) && (normalizedResource.charAt(1) != startingCharacter);
+
+ if (normalizationTester.test('/') && normalizationTester.test('^')) {
+ fail(String.format("The '%s' resource should start with a '^' character if a regular expression has matched it, '/' otherwise!", resource));
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index a82b222..5b408a9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
2.7
2.8.6
5.7.0
+ 3.6.0
@@ -106,6 +107,19 @@
${junit.version}
test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
+ test
+
+
+ org.mockito
+ mockito-inline
+ ${mockito.version}
+ test
+
@@ -193,4 +207,4 @@