View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.rest.filter;
19  
20  import java.io.IOException;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import javax.servlet.Filter;
29  import javax.servlet.FilterChain;
30  import javax.servlet.FilterConfig;
31  import javax.servlet.ServletException;
32  import javax.servlet.ServletRequest;
33  import javax.servlet.ServletResponse;
34  import javax.servlet.http.HttpServletRequest;
35  import javax.servlet.http.HttpServletResponse;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.apache.hadoop.classification.InterfaceAudience;
40  import org.apache.hadoop.classification.InterfaceStability;
41  import org.apache.hadoop.conf.Configuration;
42  
43  /**
44   * This filter provides protection against cross site request forgery (CSRF)
45   * attacks for REST APIs. Enabling this filter on an endpoint results in the
46   * requirement of all client to send a particular (configurable) HTTP header
47   * with every request. In the absense of this header the filter will reject the
48   * attempt as a bad request.
49   */
50  @InterfaceAudience.Public
51  @InterfaceStability.Evolving
52  public class RestCsrfPreventionFilter implements Filter {
53  
54    private static final Log LOG =
55        LogFactory.getLog(RestCsrfPreventionFilter.class);
56  
57    public static final String HEADER_USER_AGENT = "User-Agent";
58    public static final String BROWSER_USER_AGENT_PARAM =
59        "browser-useragents-regex";
60    public static final String CUSTOM_HEADER_PARAM = "custom-header";
61    public static final String CUSTOM_METHODS_TO_IGNORE_PARAM =
62        "methods-to-ignore";
63    static final String  BROWSER_USER_AGENTS_DEFAULT = "^Mozilla.*,^Opera.*";
64    public static final String HEADER_DEFAULT = "X-XSRF-HEADER";
65    static final String  METHODS_TO_IGNORE_DEFAULT = "GET,OPTIONS,HEAD,TRACE";
66    private String  headerName = HEADER_DEFAULT;
67    private Set<String> methodsToIgnore = null;
68    private Set<Pattern> browserUserAgents;
69  
70    @Override
71    public void init(FilterConfig filterConfig) throws ServletException {
72      String customHeader = filterConfig.getInitParameter(CUSTOM_HEADER_PARAM);
73      if (customHeader != null) {
74        headerName = customHeader;
75      }
76      String customMethodsToIgnore =
77          filterConfig.getInitParameter(CUSTOM_METHODS_TO_IGNORE_PARAM);
78      if (customMethodsToIgnore != null) {
79        parseMethodsToIgnore(customMethodsToIgnore);
80      } else {
81        parseMethodsToIgnore(METHODS_TO_IGNORE_DEFAULT);
82      }
83  
84      String agents = filterConfig.getInitParameter(BROWSER_USER_AGENT_PARAM);
85      if (agents == null) {
86        agents = BROWSER_USER_AGENTS_DEFAULT;
87      }
88      parseBrowserUserAgents(agents);
89      LOG.info(String.format("Adding cross-site request forgery (CSRF) protection, "
90          + "headerName = %s, methodsToIgnore = %s, browserUserAgents = %s",
91          headerName, methodsToIgnore, browserUserAgents));
92    }
93  
94    void parseBrowserUserAgents(String userAgents) {
95      String[] agentsArray =  userAgents.split(",");
96      browserUserAgents = new HashSet<Pattern>();
97      for (String patternString : agentsArray) {
98        browserUserAgents.add(Pattern.compile(patternString));
99      }
100   }
101 
102   void parseMethodsToIgnore(String mti) {
103     String[] methods = mti.split(",");
104     methodsToIgnore = new HashSet<String>();
105     for (int i = 0; i < methods.length; i++) {
106       methodsToIgnore.add(methods[i]);
107     }
108   }
109 
110   /**
111    * This method interrogates the User-Agent String and returns whether it
112    * refers to a browser.  If its not a browser, then the requirement for the
113    * CSRF header will not be enforced; if it is a browser, the requirement will
114    * be enforced.
115    * <p>
116    * A User-Agent String is considered to be a browser if it matches
117    * any of the regex patterns from browser-useragent-regex; the default
118    * behavior is to consider everything a browser that matches the following:
119    * "^Mozilla.*,^Opera.*".  Subclasses can optionally override
120    * this method to use different behavior.
121    *
122    * @param userAgent The User-Agent String, or null if there isn't one
123    * @return true if the User-Agent String refers to a browser, false if not
124    */
125   protected boolean isBrowser(String userAgent) {
126     if (userAgent == null) {
127       return false;
128     }
129     for (Pattern pattern : browserUserAgents) {
130       Matcher matcher = pattern.matcher(userAgent);
131       if (matcher.matches()) {
132         return true;
133       }
134     }
135     return false;
136   }
137 
138   /**
139    * Defines the minimal API requirements for the filter to execute its
140    * filtering logic.  This interface exists to facilitate integration in
141    * components that do not run within a servlet container and therefore cannot
142    * rely on a servlet container to dispatch to the {@link #doFilter} method.
143    * Applications that do run inside a servlet container will not need to write
144    * code that uses this interface.  Instead, they can use typical servlet
145    * container configuration mechanisms to insert the filter.
146    */
147   public interface HttpInteraction {
148 
149     /**
150      * Returns the value of a header.
151      *
152      * @param header name of header
153      * @return value of header
154      */
155     String getHeader(String header);
156 
157     /**
158      * Returns the method.
159      *
160      * @return method
161      */
162     String getMethod();
163 
164     /**
165      * Called by the filter after it decides that the request may proceed.
166      *
167      * @throws IOException if there is an I/O error
168      * @throws ServletException if the implementation relies on the servlet API
169      *     and a servlet API call has failed
170      */
171     void proceed() throws IOException, ServletException;
172 
173     /**
174      * Called by the filter after it decides that the request is a potential
175      * CSRF attack and therefore must be rejected.
176      *
177      * @param code status code to send
178      * @param message response message
179      * @throws IOException if there is an I/O error
180      */
181     void sendError(int code, String message) throws IOException;
182   }
183 
184   /**
185    * Handles an {@link HttpInteraction} by applying the filtering logic.
186    *
187    * @param httpInteraction caller's HTTP interaction
188    * @throws IOException if there is an I/O error
189    * @throws ServletException if the implementation relies on the servlet API
190    *     and a servlet API call has failed
191    */
192   public void handleHttpInteraction(HttpInteraction httpInteraction)
193       throws IOException, ServletException {
194     if (!isBrowser(httpInteraction.getHeader(HEADER_USER_AGENT)) ||
195         methodsToIgnore.contains(httpInteraction.getMethod()) ||
196         httpInteraction.getHeader(headerName) != null) {
197       httpInteraction.proceed();
198     } else {
199       httpInteraction.sendError(HttpServletResponse.SC_BAD_REQUEST,
200           "Missing Required Header for CSRF Vulnerability Protection");
201     }
202   }
203 
204   @Override
205   public void doFilter(ServletRequest request, ServletResponse response,
206       final FilterChain chain) throws IOException, ServletException {
207     final HttpServletRequest httpRequest = (HttpServletRequest)request;
208     final HttpServletResponse httpResponse = (HttpServletResponse)response;
209     handleHttpInteraction(new ServletFilterHttpInteraction(httpRequest,
210         httpResponse, chain));
211   }
212 
213   @Override
214   public void destroy() {
215   }
216 
217   /**
218    * Constructs a mapping of configuration properties to be used for filter
219    * initialization.  The mapping includes all properties that start with the
220    * specified configuration prefix.  Property names in the mapping are trimmed
221    * to remove the configuration prefix.
222    *
223    * @param conf configuration to read
224    * @param confPrefix configuration prefix
225    * @return mapping of configuration properties to be used for filter
226    *     initialization
227    */
228   public static Map<String, String> getFilterParams(Configuration conf,
229       String confPrefix) {
230     Map<String, String> filterConfigMap = new HashMap<>();
231     for (Map.Entry<String, String> entry : conf) {
232       String name = entry.getKey();
233       if (name.startsWith(confPrefix)) {
234         String value = conf.get(name);
235         name = name.substring(confPrefix.length());
236         filterConfigMap.put(name, value);
237       }
238     }
239     return filterConfigMap;
240   }
241 
242   /**
243    * {@link HttpInteraction} implementation for use in the servlet filter.
244    */
245   private static final class ServletFilterHttpInteraction
246       implements HttpInteraction {
247 
248     private final FilterChain chain;
249     private final HttpServletRequest httpRequest;
250     private final HttpServletResponse httpResponse;
251 
252     /**
253      * Creates a new ServletFilterHttpInteraction.
254      *
255      * @param httpRequest request to process
256      * @param httpResponse response to process
257      * @param chain filter chain to forward to if HTTP interaction is allowed
258      */
259     public ServletFilterHttpInteraction(HttpServletRequest httpRequest,
260         HttpServletResponse httpResponse, FilterChain chain) {
261       this.httpRequest = httpRequest;
262       this.httpResponse = httpResponse;
263       this.chain = chain;
264     }
265 
266     @Override
267     public String getHeader(String header) {
268       return httpRequest.getHeader(header);
269     }
270 
271     @Override
272     public String getMethod() {
273       return httpRequest.getMethod();
274     }
275 
276     @Override
277     public void proceed() throws IOException, ServletException {
278       chain.doFilter(httpRequest, httpResponse);
279     }
280 
281     @Override
282     public void sendError(int code, String message) throws IOException {
283       httpResponse.sendError(code, message);
284     }
285   }
286 }