diff --git a/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties b/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties index 9fa02ce6..4b3e6fa8 100644 --- a/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties +++ b/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties @@ -136,7 +136,11 @@ user.manager.impl=jdo # REST security settings # REST base url is for avoiding CSRF attacks +# Enable CSRF filtering +rest.csrffilter.enabled=true # If it is not set or empty it tries to determine the base url automatically rest.baseUrl= # If true, requests without Origin or Referer Header are denied -rest.csrf.absentorigin.deny=true \ No newline at end of file +rest.csrffilter.absentorigin.deny=true +# If true, the validation of the CSRF tokens will be disabled +rest.csrffilter.disableTokenValidation=false \ No newline at end of file diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java index 182d23a0..a9afedea 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java @@ -19,7 +19,17 @@ package org.apache.archiva.redback.rest.services.interceptors; */ +import org.apache.archiva.redback.authentication.AuthenticationException; +import org.apache.archiva.redback.authentication.AuthenticationResult; +import org.apache.archiva.redback.authentication.InvalidTokenException; +import org.apache.archiva.redback.authentication.TokenData; +import org.apache.archiva.redback.authentication.TokenManager; +import org.apache.archiva.redback.authorization.RedbackAuthorization; import org.apache.archiva.redback.configuration.UserConfiguration; +import org.apache.archiva.redback.integration.filter.authentication.basic.HttpBasicAuthentication; +import org.apache.archiva.redback.policy.AccountLockedException; +import org.apache.archiva.redback.policy.MustChangePasswordException; +import org.apache.archiva.redback.users.User; import org.apache.cxf.jaxrs.utils.JAXRSUtils; import org.apache.cxf.message.Message; import org.slf4j.Logger; @@ -60,15 +70,18 @@ public class RequestValidationInterceptor extends AbstractInterceptor implements private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; private static final String X_FORWARDED_HOST = "X-Forwarded-Host"; + private static final String X_XSRF_TOKEN = "X-XSRF-TOKEN"; private static final String ORIGIN = "Origin"; private static final String REFERER = "Referer"; - private static final String CFG_REST_BASE_URL = "rest.baseUrl"; - private static final String CFG_REST_CSRF_ABSENTORIGIN_DENY = "rest.csrffilter.absentorigin.deny"; - private static final String CFG_REST_CSRF_ENABLED = "rest.csrffilter.enabled"; + public static final String CFG_REST_BASE_URL = "rest.baseUrl"; + public static final String CFG_REST_CSRF_ABSENTORIGIN_DENY = "rest.csrffilter.absentorigin.deny"; + public static final String CFG_REST_CSRF_ENABLED = "rest.csrffilter.enabled"; + public static final String CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION = "rest.csrffilter.disableTokenValidation"; private final Logger log = LoggerFactory.getLogger( getClass() ); private boolean enabled = true; + private boolean checkToken = true; private boolean useStaticUrl = false; private boolean denyAbsentHeaders = true; private URL baseUrl; @@ -76,6 +89,14 @@ public class RequestValidationInterceptor extends AbstractInterceptor implements private UserConfiguration config; + @Inject + @Named( value = "httpAuthenticator#basic" ) + private HttpBasicAuthentication httpAuthenticator; + + @Inject + @Named( value = "tokenManager#default") + TokenManager tokenManager; + @Inject public RequestValidationInterceptor(@Named( value = "userConfiguration#default" ) UserConfiguration config) { @@ -100,6 +121,7 @@ public class RequestValidationInterceptor extends AbstractInterceptor implements if (!enabled) { log.info("CSRF Filter is disabled by configuration"); } + checkToken = !config.getBoolean(CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION, false); } @Override @@ -110,14 +132,60 @@ public class RequestValidationInterceptor extends AbstractInterceptor implements if (targetUrl == null) { log.error("Could not verify target URL."); containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + return; } if (!checkSourceRequestHeader(targetUrl, request)) { log.warn("HTTP Header check failed. Assuming CSRF attack."); containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + return; + } + + if (checkToken) { + checkValidationToken(containerRequestContext, request); } } } + private void checkValidationToken(ContainerRequestContext containerRequestContext, HttpServletRequest request) { + Message message = JAXRSUtils.getCurrentMessage(); + RedbackAuthorization redbackAuthorization = getRedbackAuthorization(message); + // We check only services that are restricted + if (!redbackAuthorization.noRestriction()) { + String tokenString = request.getHeader(X_XSRF_TOKEN); + if (tokenString==null || tokenString.length()==0) { + log.warn("No validation token header found: {}",X_XSRF_TOKEN); + containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + return; + } + + try { + TokenData td = tokenManager.decryptToken(tokenString); + AuthenticationResult auth = getAuthenticationResult(message, request); + if (auth==null) { + log.error("Not authentication data found"); + containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + return; + } + User loggedIn = auth.getUser(); + if (loggedIn==null) { + log.error("User not logged in"); + containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + return; + } + String username = loggedIn.getUsername(); + if (!td.isValid() || !td.getUser().equals(username)) { + log.error("Invalid data in validation token header {} for user {}: isValid={}, username={}", + X_XSRF_TOKEN, username, td.isValid(), td.getUser()); + containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + } + } catch (InvalidTokenException e) { + log.error("Token validation failed {}", e.getMessage()); + containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + } + } + log.debug("Token validated"); + } + private HttpServletRequest getRequest() { if (httpRequest!=null) { return httpRequest; @@ -215,4 +283,33 @@ public class RequestValidationInterceptor extends AbstractInterceptor implements public void setHttpRequest(HttpServletRequest request) { this.httpRequest = request; } + + private AuthenticationResult getAuthenticationResult(Message message, HttpServletRequest request) { + AuthenticationResult authenticationResult = message.get(AuthenticationResult.class); + + log.debug("authenticationResult from message: {}", authenticationResult); + if ( authenticationResult == null ) + { + try + { + authenticationResult = + httpAuthenticator.getAuthenticationResult( request, getHttpServletResponse( message ) ); + + log.debug( "authenticationResult from request: {}", authenticationResult ); + } + catch ( AuthenticationException e ) + { + log.debug( "failed to authenticate for path {}", message.get( Message.REQUEST_URI ) ); + } + catch ( AccountLockedException e ) + { + log.debug( "account locked for path {}", message.get( Message.REQUEST_URI ) ); + } + catch ( MustChangePasswordException e ) + { + log.debug( "must change password for path {}", message.get( Message.REQUEST_URI ) ); + } + } + return authenticationResult; + } } diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/RequestValidationInterceptorTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/RequestValidationInterceptorTest.java index c88492a0..27ab5317 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/RequestValidationInterceptorTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/RequestValidationInterceptorTest.java @@ -20,10 +20,12 @@ package org.apache.archiva.redback.rest.services; import junit.framework.TestCase; +import org.apache.archiva.redback.authentication.TokenManager; import org.apache.archiva.redback.configuration.UserConfigurationException; import org.apache.archiva.redback.rest.services.interceptors.RequestValidationInterceptor; import org.apache.archiva.redback.rest.services.mock.MockContainerRequestContext; import org.apache.archiva.redback.rest.services.mock.MockUserConfiguration; +import org.apache.archiva.redback.system.SecuritySystem; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -36,7 +38,7 @@ import java.io.IOException; /** * Created by Martin Stockhammer on 21.01.17. * - * Unit Test for RequestValidationInterceptor. + * Unit Test for RequestValidationInterceptor. The unit tests are all without token validation. * */ @RunWith(JUnit4.class) @@ -46,7 +48,9 @@ public class RequestValidationInterceptorTest extends TestCase { @Test public void validateRequestWithoutHeader() throws UserConfigurationException, IOException { + TokenManager tm = new TokenManager(); MockUserConfiguration cfg = new MockUserConfiguration(); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest(); interceptor.setHttpRequest(request); @@ -58,7 +62,9 @@ public class RequestValidationInterceptorTest extends TestCase { @Test public void validateRequestWithOrigin() throws UserConfigurationException, IOException { + TokenManager tm = new TokenManager(); MockUserConfiguration cfg = new MockUserConfiguration(); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest("GET","/api/v1/userService"); request.setServerName("test.archiva.org"); @@ -72,7 +78,9 @@ public class RequestValidationInterceptorTest extends TestCase { @Test public void validateRequestWithBadOrigin() throws UserConfigurationException, IOException { + TokenManager tm = new TokenManager(); MockUserConfiguration cfg = new MockUserConfiguration(); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest("GET","/api/v1/userService"); request.setServerName("test.archiva.org"); @@ -86,7 +94,9 @@ public class RequestValidationInterceptorTest extends TestCase { @Test public void validateRequestWithReferer() throws UserConfigurationException, IOException { + TokenManager tm = new TokenManager(); MockUserConfiguration cfg = new MockUserConfiguration(); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest("GET","/api/v1/userService"); request.setServerName("test.archiva.org"); @@ -100,7 +110,9 @@ public class RequestValidationInterceptorTest extends TestCase { @Test public void validateRequestWithBadReferer() throws UserConfigurationException, IOException { + TokenManager tm = new TokenManager(); MockUserConfiguration cfg = new MockUserConfiguration(); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest("GET","/api/v1/userService"); request.setServerName("test.archiva.org"); @@ -114,7 +126,9 @@ public class RequestValidationInterceptorTest extends TestCase { @Test public void validateRequestWithOriginAndReferer() throws UserConfigurationException, IOException { + TokenManager tm = new TokenManager(); MockUserConfiguration cfg = new MockUserConfiguration(); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest("GET","/api/v1/userService"); request.setServerName("test.archiva.org"); @@ -132,6 +146,8 @@ public class RequestValidationInterceptorTest extends TestCase { public void validateRequestWithOriginAndStaticUrl() throws UserConfigurationException, IOException { MockUserConfiguration cfg = new MockUserConfiguration(); cfg.addValue("rest.baseUrl","http://test.archiva.org"); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); + TokenManager tm = new TokenManager(); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest("GET","/api/v1/userService"); request.setServerName("test4.archiva.org"); @@ -147,6 +163,8 @@ public class RequestValidationInterceptorTest extends TestCase { public void validateRequestWithBadOriginAndStaticUrl() throws UserConfigurationException, IOException { MockUserConfiguration cfg = new MockUserConfiguration(); cfg.addValue("rest.baseUrl","http://mytest.archiva.org"); + cfg.addValue(RequestValidationInterceptor.CFG_REST_CSRF_DISABLE_TOKEN_VALIDATION,"true"); + TokenManager tm = new TokenManager(); RequestValidationInterceptor interceptor = new RequestValidationInterceptor(cfg); MockHttpServletRequest request = new MockHttpServletRequest("GET","/api/v1/userService"); request.setServerName("mytest.archiva.org");