-
Notifications
You must be signed in to change notification settings - Fork 565
/
AntiCsrfHelper.java
154 lines (129 loc) · 5.58 KB
/
AntiCsrfHelper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.security.authc;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType;
import org.sonatype.goodies.common.ComponentSupport;
import org.sonatype.nexus.common.text.Strings2;
import org.apache.shiro.authz.UnauthorizedException;
/**
* @since 3.16
*/
@Named
@Singleton
public class AntiCsrfHelper extends ComponentSupport
{
public static final String ENABLED = "nexus.security.anticsrftoken.enabled";
public static final String ERROR_MESSAGE_TOKEN_MISMATCH = "Anti cross-site request forgery token mismatch";
public static final String ANTI_CSRF_TOKEN_NAME = "NX-ANTI-CSRF-TOKEN";
private final List<String> matchers;
private final boolean enabled;
@Inject
public AntiCsrfHelper(
@Named("${nexus.security.anticsrftoken.enabled:-true}") final boolean enabled,
@Named("${nexus.security.anticsrftoken.whitelist}") @Nullable final String whitelist)
{
this.enabled = enabled;
this.matchers = enabled ? createUserAgentMatchers(whitelist) : Collections.emptyList();
}
private List<String> createUserAgentMatchers(final String whitelist) {
List<String> matchers = new ArrayList<>();
matchers.add("WindowsPowerShell");
if (!Strings2.isBlank(whitelist)) {
for (String substring : whitelist.split(",")) {
if (!Strings2.isBlank(substring)) {
matchers.add(substring);
}
}
}
return matchers;
}
/**
* Checks the request for CSRF if the token is invalid.
*
* @return true if the token is valid or if the token does not require validation. Requests with a multipart form
* content type should call {@link requireValidToken} once the field is extracted.
*/
public boolean isAccessAllowed(final HttpServletRequest httpRequest) {
if (!enabled) {
return true;
}
return isSafeHttpMethod(httpRequest)
|| isMultiPartFormDataPost(httpRequest) // token is passed as a form field instead of a custom header
// and is validated in the directnjine code so we just needed
// to create the cookie above
|| isNotBrowserRequest(httpRequest)
|| isAntiCsrfTokenValid(httpRequest, Optional.ofNullable(httpRequest.getHeader(ANTI_CSRF_TOKEN_NAME)));
}
/**
* Validate that the token passed as an argument matches the cookie in the request (if the request requires
* validation)
*
* @throws UnauthorizedException when the provided token is missing or does not match the request
*/
public void requireValidToken(final HttpServletRequest httpRequest, @Nullable final String token) {
Optional<String> optToken = token == null ? Optional.ofNullable(httpRequest.getHeader(ANTI_CSRF_TOKEN_NAME))
: Optional.of(token);
if (!enabled || isNotBrowserRequest(httpRequest) || isAntiCsrfTokenValid(httpRequest, optToken)) {
return;
}
throw new UnauthorizedException(ERROR_MESSAGE_TOKEN_MISMATCH);
}
private boolean isSafeHttpMethod(final HttpServletRequest request) {
String method = request.getMethod();
return HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method);
}
private boolean isMultiPartFormDataPost(final HttpServletRequest request) {
return HttpMethod.POST.equals(request.getMethod()) && !Strings2.isBlank(request.getContentType())
&& MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(MediaType.valueOf(request.getContentType()));
}
private boolean isNotBrowserRequest(final HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
return userAgent == null || !userAgent.startsWith("Mozilla/")
|| matchers.stream().anyMatch(m -> userAgent.contains(m));
}
private Optional<String> getCookie(final HttpServletRequest request, final String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
return Optional.ofNullable(cookie.getValue());
}
}
}
return Optional.empty();
}
private Optional<String> getAntiCsrfTokenCookie(final HttpServletRequest request) {
return getCookie(request, ANTI_CSRF_TOKEN_NAME);
}
private boolean isAntiCsrfTokenValid(final HttpServletRequest request, final Optional<String> token) {
Optional<String> cookie = getAntiCsrfTokenCookie(request);
return token.isPresent() && token.equals(cookie);
}
/**
* @return whether CSRF protection is enabled
*/
public boolean isEnabled() {
return enabled;
}
}