HADOOP-12758. Extend CSRF Filter with UserAgent Checks. Contributed by Larry McCay.
(cherry picked from commita37e423e84
) (cherry picked from commit5752df2362
)
This commit is contained in:
parent
150240cce0
commit
e01d8393b6
|
@ -425,6 +425,9 @@ Release 2.8.0 - UNRELEASED
|
||||||
HADOOP-12450. UserGroupInformation should not log at WARN level if no groups
|
HADOOP-12450. UserGroupInformation should not log at WARN level if no groups
|
||||||
are found. (Elliott Clark via stevel)
|
are found. (Elliott Clark via stevel)
|
||||||
|
|
||||||
|
HADOOP-12758. Extend CSRF Filter with UserAgent Checks
|
||||||
|
(Larry McCay via cnauroth)
|
||||||
|
|
||||||
BUG FIXES
|
BUG FIXES
|
||||||
|
|
||||||
HADOOP-12617. SPNEGO authentication request to non-default realm gets
|
HADOOP-12617. SPNEGO authentication request to non-default realm gets
|
||||||
|
|
|
@ -19,6 +19,8 @@ package org.apache.hadoop.security.http;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
|
@ -38,13 +40,18 @@ import javax.servlet.http.HttpServletResponse;
|
||||||
* attempt as a bad request.
|
* attempt as a bad request.
|
||||||
*/
|
*/
|
||||||
public class RestCsrfPreventionFilter implements Filter {
|
public class RestCsrfPreventionFilter implements Filter {
|
||||||
|
public static final String HEADER_USER_AGENT = "User-Agent";
|
||||||
|
public static final String BROWSER_USER_AGENT_PARAM =
|
||||||
|
"browser-useragents-regex";
|
||||||
public static final String CUSTOM_HEADER_PARAM = "custom-header";
|
public static final String CUSTOM_HEADER_PARAM = "custom-header";
|
||||||
public static final String CUSTOM_METHODS_TO_IGNORE_PARAM =
|
public static final String CUSTOM_METHODS_TO_IGNORE_PARAM =
|
||||||
"methods-to-ignore";
|
"methods-to-ignore";
|
||||||
|
static final String BROWSER_USER_AGENTS_DEFAULT = "^Mozilla.*,^Opera.*";
|
||||||
static final String HEADER_DEFAULT = "X-XSRF-HEADER";
|
static final String HEADER_DEFAULT = "X-XSRF-HEADER";
|
||||||
static final String METHODS_TO_IGNORE_DEFAULT = "GET,OPTIONS,HEAD,TRACE";
|
static final String METHODS_TO_IGNORE_DEFAULT = "GET,OPTIONS,HEAD,TRACE";
|
||||||
private String headerName = HEADER_DEFAULT;
|
private String headerName = HEADER_DEFAULT;
|
||||||
private Set<String> methodsToIgnore = null;
|
private Set<String> methodsToIgnore = null;
|
||||||
|
private Set<Pattern> browserUserAgents;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(FilterConfig filterConfig) throws ServletException {
|
public void init(FilterConfig filterConfig) throws ServletException {
|
||||||
|
@ -59,6 +66,20 @@ public class RestCsrfPreventionFilter implements Filter {
|
||||||
} else {
|
} else {
|
||||||
parseMethodsToIgnore(METHODS_TO_IGNORE_DEFAULT);
|
parseMethodsToIgnore(METHODS_TO_IGNORE_DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String agents = filterConfig.getInitParameter(BROWSER_USER_AGENT_PARAM);
|
||||||
|
if (agents == null) {
|
||||||
|
agents = BROWSER_USER_AGENTS_DEFAULT;
|
||||||
|
}
|
||||||
|
parseBrowserUserAgents(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseBrowserUserAgents(String userAgents) {
|
||||||
|
String[] agentsArray = userAgents.split(",");
|
||||||
|
browserUserAgents = new HashSet<Pattern>();
|
||||||
|
for (String patternString : agentsArray) {
|
||||||
|
browserUserAgents.add(Pattern.compile(patternString));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseMethodsToIgnore(String mti) {
|
void parseMethodsToIgnore(String mti) {
|
||||||
|
@ -69,17 +90,46 @@ public class RestCsrfPreventionFilter implements Filter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method interrogates the User-Agent String and returns whether it
|
||||||
|
* refers to a browser. If its not a browser, then the requirement for the
|
||||||
|
* CSRF header will not be enforced; if it is a browser, the requirement will
|
||||||
|
* be enforced.
|
||||||
|
* <p>
|
||||||
|
* A User-Agent String is considered to be a browser if it matches
|
||||||
|
* any of the regex patterns from browser-useragent-regex; the default
|
||||||
|
* behavior is to consider everything a browser that matches the following:
|
||||||
|
* "^Mozilla.*,^Opera.*". Subclasses can optionally override
|
||||||
|
* this method to use different behavior.
|
||||||
|
*
|
||||||
|
* @param userAgent The User-Agent String, or null if there isn't one
|
||||||
|
* @return true if the User-Agent String refers to a browser, false if not
|
||||||
|
*/
|
||||||
|
protected boolean isBrowser(String userAgent) {
|
||||||
|
if (userAgent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Pattern pattern : browserUserAgents) {
|
||||||
|
Matcher matcher = pattern.matcher(userAgent);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest request, ServletResponse response,
|
public void doFilter(ServletRequest request, ServletResponse response,
|
||||||
FilterChain chain) throws IOException, ServletException {
|
FilterChain chain) throws IOException, ServletException {
|
||||||
HttpServletRequest httpRequest = (HttpServletRequest)request;
|
HttpServletRequest httpRequest = (HttpServletRequest)request;
|
||||||
if (methodsToIgnore.contains(httpRequest.getMethod()) ||
|
if (!isBrowser(httpRequest.getHeader(HEADER_USER_AGENT)) ||
|
||||||
|
methodsToIgnore.contains(httpRequest.getMethod()) ||
|
||||||
httpRequest.getHeader(headerName) != null) {
|
httpRequest.getHeader(headerName) != null) {
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
} else {
|
} else {
|
||||||
((HttpServletResponse)response).sendError(
|
((HttpServletResponse)response).sendError(
|
||||||
HttpServletResponse.SC_BAD_REQUEST,
|
HttpServletResponse.SC_BAD_REQUEST,
|
||||||
"Missing Required Header for Vulnerability Protection");
|
"Missing Required Header for CSRF Vulnerability Protection");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,12 @@ import org.mockito.Mockito;
|
||||||
|
|
||||||
public class TestRestCsrfPreventionFilter {
|
public class TestRestCsrfPreventionFilter {
|
||||||
|
|
||||||
|
private static final String NON_BROWSER = "java";
|
||||||
|
private static final String BROWSER_AGENT =
|
||||||
|
"Mozilla/5.0 (compatible; U; ABrowse 0.6; Syllable)" +
|
||||||
|
" AppleWebKit/420+ (KHTML, like Gecko)";
|
||||||
private static final String EXPECTED_MESSAGE =
|
private static final String EXPECTED_MESSAGE =
|
||||||
"Missing Required Header for Vulnerability Protection";
|
"Missing Required Header for CSRF Vulnerability Protection";
|
||||||
private static final String X_CUSTOM_HEADER = "X-CUSTOM_HEADER";
|
private static final String X_CUSTOM_HEADER = "X-CUSTOM_HEADER";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -52,7 +56,9 @@ public class TestRestCsrfPreventionFilter {
|
||||||
// CSRF has not been sent
|
// CSRF has not been sent
|
||||||
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
thenReturn(null);
|
thenReturn(null);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn(BROWSER_AGENT);
|
||||||
|
|
||||||
// Objects to verify interactions based on request
|
// Objects to verify interactions based on request
|
||||||
HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class);
|
HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class);
|
||||||
|
@ -68,6 +74,71 @@ public class TestRestCsrfPreventionFilter {
|
||||||
Mockito.verifyZeroInteractions(mockChain);
|
Mockito.verifyZeroInteractions(mockChain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoHeaderCustomAgentConfig_badRequest()
|
||||||
|
throws ServletException, IOException {
|
||||||
|
// Setup the configuration settings of the server
|
||||||
|
FilterConfig filterConfig = Mockito.mock(FilterConfig.class);
|
||||||
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
|
RestCsrfPreventionFilter.CUSTOM_HEADER_PARAM)).thenReturn(null);
|
||||||
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
|
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
||||||
|
thenReturn(null);
|
||||||
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
|
RestCsrfPreventionFilter.BROWSER_USER_AGENT_PARAM)).
|
||||||
|
thenReturn("^Mozilla.*,^Opera.*,curl");
|
||||||
|
|
||||||
|
// CSRF has not been sent
|
||||||
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
|
thenReturn(null);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn("curl");
|
||||||
|
|
||||||
|
// Objects to verify interactions based on request
|
||||||
|
HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class);
|
||||||
|
FilterChain mockChain = Mockito.mock(FilterChain.class);
|
||||||
|
|
||||||
|
// Object under test
|
||||||
|
RestCsrfPreventionFilter filter = new RestCsrfPreventionFilter();
|
||||||
|
filter.init(filterConfig);
|
||||||
|
filter.doFilter(mockReq, mockRes, mockChain);
|
||||||
|
|
||||||
|
verify(mockRes, atLeastOnce()).sendError(
|
||||||
|
HttpServletResponse.SC_BAD_REQUEST, EXPECTED_MESSAGE);
|
||||||
|
Mockito.verifyZeroInteractions(mockChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoHeaderDefaultConfigNonBrowser_goodRequest()
|
||||||
|
throws ServletException, IOException {
|
||||||
|
// Setup the configuration settings of the server
|
||||||
|
FilterConfig filterConfig = Mockito.mock(FilterConfig.class);
|
||||||
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
|
RestCsrfPreventionFilter.CUSTOM_HEADER_PARAM)).thenReturn(null);
|
||||||
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
|
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
||||||
|
thenReturn(null);
|
||||||
|
|
||||||
|
// CSRF has not been sent
|
||||||
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
|
thenReturn(null);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn(NON_BROWSER);
|
||||||
|
|
||||||
|
// Objects to verify interactions based on request
|
||||||
|
HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class);
|
||||||
|
FilterChain mockChain = Mockito.mock(FilterChain.class);
|
||||||
|
|
||||||
|
// Object under test
|
||||||
|
RestCsrfPreventionFilter filter = new RestCsrfPreventionFilter();
|
||||||
|
filter.init(filterConfig);
|
||||||
|
filter.doFilter(mockReq, mockRes, mockChain);
|
||||||
|
|
||||||
|
Mockito.verify(mockChain).doFilter(mockReq, mockRes);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHeaderPresentDefaultConfig_goodRequest()
|
public void testHeaderPresentDefaultConfig_goodRequest()
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
@ -136,9 +207,11 @@ public class TestRestCsrfPreventionFilter {
|
||||||
Mockito.when(filterConfig.getInitParameter(
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
||||||
thenReturn(null);
|
thenReturn(null);
|
||||||
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn(BROWSER_AGENT);
|
||||||
|
|
||||||
// CSRF has not been sent
|
// CSRF has not been sent
|
||||||
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
|
||||||
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
thenReturn(null);
|
thenReturn(null);
|
||||||
|
|
||||||
|
@ -164,9 +237,11 @@ public class TestRestCsrfPreventionFilter {
|
||||||
Mockito.when(filterConfig.getInitParameter(
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
||||||
thenReturn("");
|
thenReturn("");
|
||||||
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn(BROWSER_AGENT);
|
||||||
|
|
||||||
// CSRF has not been sent
|
// CSRF has not been sent
|
||||||
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
|
||||||
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
thenReturn(null);
|
thenReturn(null);
|
||||||
Mockito.when(mockReq.getMethod()).
|
Mockito.when(mockReq.getMethod()).
|
||||||
|
@ -194,9 +269,11 @@ public class TestRestCsrfPreventionFilter {
|
||||||
Mockito.when(filterConfig.getInitParameter(
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
||||||
thenReturn("GET");
|
thenReturn("GET");
|
||||||
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn(BROWSER_AGENT);
|
||||||
|
|
||||||
// CSRF has not been sent
|
// CSRF has not been sent
|
||||||
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
|
||||||
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
thenReturn(null);
|
thenReturn(null);
|
||||||
Mockito.when(mockReq.getMethod()).
|
Mockito.when(mockReq.getMethod()).
|
||||||
|
@ -224,9 +301,11 @@ public class TestRestCsrfPreventionFilter {
|
||||||
Mockito.when(filterConfig.getInitParameter(
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
||||||
thenReturn("GET,OPTIONS");
|
thenReturn("GET,OPTIONS");
|
||||||
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn(BROWSER_AGENT);
|
||||||
|
|
||||||
// CSRF has not been sent
|
// CSRF has not been sent
|
||||||
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
|
||||||
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
thenReturn(null);
|
thenReturn(null);
|
||||||
Mockito.when(mockReq.getMethod()).
|
Mockito.when(mockReq.getMethod()).
|
||||||
|
@ -254,9 +333,11 @@ public class TestRestCsrfPreventionFilter {
|
||||||
Mockito.when(filterConfig.getInitParameter(
|
Mockito.when(filterConfig.getInitParameter(
|
||||||
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
RestCsrfPreventionFilter.CUSTOM_METHODS_TO_IGNORE_PARAM)).
|
||||||
thenReturn("GET,OPTIONS");
|
thenReturn("GET,OPTIONS");
|
||||||
|
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
||||||
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_USER_AGENT)).
|
||||||
|
thenReturn(BROWSER_AGENT);
|
||||||
|
|
||||||
// CSRF has not been sent
|
// CSRF has not been sent
|
||||||
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
|
|
||||||
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
Mockito.when(mockReq.getHeader(RestCsrfPreventionFilter.HEADER_DEFAULT)).
|
||||||
thenReturn(null);
|
thenReturn(null);
|
||||||
Mockito.when(mockReq.getMethod()).
|
Mockito.when(mockReq.getMethod()).
|
||||||
|
|
Loading…
Reference in New Issue