package teammates.ui.webapi.action; import java.io.IOException; import java.util.Arrays; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import teammates.common.exception.InvalidParametersException; import teammates.common.util.Config; import teammates.common.util.Const; import teammates.common.util.HttpRequestHelper; import teammates.common.util.Logger; import teammates.common.util.StringHelper; import teammates.common.util.Url; /** * Checks and validates origin of HTTP requests. */ public class OriginCheckFilter implements Filter { private static final Logger log = Logger.getLogger(); private static final String ALLOWED_HTTP_METHODS = String.join(", ", Arrays.asList( HttpGet.METHOD_NAME, HttpPost.METHOD_NAME, HttpPut.METHOD_NAME, HttpDelete.METHOD_NAME, HttpOptions.METHOD_NAME )); private static final String ALLOWED_HEADERS = String.join(", ", Arrays.asList( Const.CsrfConfig.TOKEN_HEADER_NAME, "Content-Type" )); @Override public void init(FilterConfig filterConfig) { // nothing to do } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (Config.isDevServer()) { response.setHeader("Access-Control-Allow-Origin", Config.APP_FRONTENDDEV_URL); response.setHeader("Access-Control-Allow-Methods", ALLOWED_HTTP_METHODS); response.setHeader("Access-Control-Allow-Headers", ALLOWED_HEADERS); response.setHeader("Access-Control-Allow-Credentials", "true"); } if (Config.CSRF_KEY.equals(request.getHeader("CSRF-Key"))) { // Can bypass CSRF check with the correct key chain.doFilter(req, res); return; } String referrer = request.getHeader("referer"); if (referrer == null) { // Requests with missing referrer information are given the benefit of the doubt // to accommodate users who choose to disable the HTTP referrer setting in their browser // for privacy reasons } else if (!isHttpReferrerValid(referrer, request.getRequestURL().toString())) { denyAccess("Invalid HTTP referrer.", request, response); return; } switch (request.getMethod()) { case HttpPost.METHOD_NAME: case HttpPut.METHOD_NAME: case HttpDelete.METHOD_NAME: String message = getCsrfTokenErrorIfAny(request); if (message != null) { denyAccess(message, request, response); return; } break; default: break; } chain.doFilter(req, res); } /** * Validates the HTTP referrer against the request URL. * The origin is the base URL of the HTTP referrer, which includes the protocol and authority * (host name + port number if specified). * Similarly, the target is the base URL of the requested action URL. * For the referrer to be considered valid, origin and target must match exactly. * Otherwise, the request is likely to be a CSRF attack, and is considered invalid. * * <p>Example of malicious request originating from embedded image in email: * <pre> * Request URL: https://teammatesv4.appspot.com/page/instructorCourseDelete?courseid=abcdef * Referrer: https://mail.google.com/mail/u/0/ * * Target: https://teammatesv4.appspot.com * Origin: https://mail.google.com * </pre> * Origin does not match target. This request is invalid.</p> * * <p>Example of legitimate request originating from instructor courses page: * <pre> * Request URL: https://teammatesv4.appspot.com/page/instructorCourseDelete?courseid=abcdef * Referrer: https://teammatesv4.appspot.com/page/instructorCoursesPage * * Target: https://teammatesv4.appspot.com * Origin: https://teammatesv4.appspot.com * </pre> * Origin matches target. This request is valid.</p> */ private boolean isHttpReferrerValid(String referrer, String requestUrl) { String origin; try { origin = new Url(referrer).getBaseUrl(); } catch (AssertionError e) { // due to MalformedURLException return false; } if (Config.isDevServer() && origin.equals(Config.APP_FRONTENDDEV_URL)) { // Exception to the rule: front-end dev server requesting data from back-end dev server return true; } String target = new Url(requestUrl).getBaseUrl(); return origin.equals(target); } private String getCsrfTokenErrorIfAny(HttpServletRequest request) { String csrfToken = request.getHeader(Const.CsrfConfig.TOKEN_HEADER_NAME); if (csrfToken == null || csrfToken.isEmpty()) { return "Missing CSRF token."; } String sessionId = request.getRequestedSessionId(); if (sessionId == null) { // Newly-created session sessionId = request.getSession().getId(); } try { return sessionId.startsWith(StringHelper.decrypt(csrfToken)) ? null : "Invalid CSRF token."; } catch (InvalidParametersException e) { return "Invalid CSRF token."; } } private void denyAccess(String message, HttpServletRequest request, HttpServletResponse response) throws IOException { response.setHeader("Strict-Transport-Security", "max-age=31536000"); log.info("Request failed origin check: [" + request.getMethod() + "] " + request.getRequestURL().toString() + ", Params: " + HttpRequestHelper.getRequestParametersAsString(request) + ", Headers: " + HttpRequestHelper.getRequestHeadersAsString(request) + ", Request ID: " + Config.getRequestId()); JsonResult result = new JsonResult(message, HttpStatus.SC_FORBIDDEN); result.send(response); } @Override public void destroy() { // nothing to do } }