Ard Schrijvers pushed to branch release/2.24 at cms-community / hippo-cms
Commits: a813da1a by Ard Schrijvers at 2016-02-09T13:51:25+01:00 :CMS-9919 copyright - - - - - 722d6233 by Ard Schrijvers at 2016-02-10T16:39:00+01:00 CMS-9919 add support to short circuit cross origin requests and add support for origin whitelisting - - - - - 3c04b37a by Ard Schrijvers at 2016-02-11T14:02:31+01:00 CMS-9919 adjust license header to account for the original wicket based logic which is still preserved - - - - - d54f5eb2 by Ard Schrijvers at 2016-03-09T10:31:42+01:00 CMS-9919 Reintegrate bugfix/CMS-9919 - - - - - 3 changed files: - engine/src/main/java/org/hippoecm/frontend/Main.java - + engine/src/main/java/org/hippoecm/frontend/http/CsrfPreventionWebRequestCycle.java - pom.xml Changes: ===================================== engine/src/main/java/org/hippoecm/frontend/Main.java ===================================== --- a/engine/src/main/java/org/hippoecm/frontend/Main.java +++ b/engine/src/main/java/org/hippoecm/frontend/Main.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2013 Hippo B.V. (http://www.onehippo.com) + * Copyright 2008-2016 Hippo B.V. (http://www.onehippo.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.hippoecm.frontend; import java.io.IOException; import java.net.URL; -import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedList; @@ -30,6 +29,7 @@ import javax.jcr.SimpleCredentials; import javax.jcr.observation.EventListener; import javax.jcr.observation.EventListenerIterator; +import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.wicket.Application; import org.apache.wicket.IRequestTarget; @@ -48,7 +48,6 @@ import org.apache.wicket.request.IRequestCycleProcessor; import org.apache.wicket.request.RequestParameters; import org.apache.wicket.request.target.coding.AbstractRequestTargetUrlCodingStrategy; import org.apache.wicket.request.target.component.BookmarkablePageRequestTarget; -import org.apache.wicket.resource.loader.IStringResourceLoader; import org.apache.wicket.session.ISessionStore; import org.apache.wicket.session.pagemap.LeastRecentlyAccessedEvictionStrategy; import org.apache.wicket.settings.IExceptionSettings; @@ -56,6 +55,7 @@ import org.apache.wicket.settings.IResourceSettings; import org.apache.wicket.util.lang.Bytes; import org.apache.wicket.util.string.StringValueConversionException; import org.apache.wicket.util.time.Duration; +import org.hippoecm.frontend.http.CsrfPreventionWebRequestCycle; import org.hippoecm.frontend.model.JcrHelper; import org.hippoecm.frontend.model.JcrNodeModel; import org.hippoecm.frontend.model.UserCredentials; @@ -90,6 +90,9 @@ public class Main extends PluginApplication { public final static String OUTPUT_WICKETPATHS = "output-wicketpaths"; public final static String PLUGIN_APPLICATION_NAME_PARAMETER = "config"; + // comma separated init parameter + public final static String ACCEPTED_ORIGIN_WHITELIST = "accepted-origin-whitelist"; + /** * Wicket RequestCycleSettings timeout configuration parameter name in development mode. */ @@ -107,6 +110,9 @@ public class Main extends PluginApplication { private HippoRepository repository; + // array of accepted origins or {@code null} if no configured + private String[] acceptedOrigins; + @Override protected void init() { super.init(); @@ -117,6 +123,8 @@ public class Main extends PluginApplication { getSessionSettings().setPageMapEvictionStrategy(new LeastRecentlyAccessedEvictionStrategy(1)); getApplicationSettings().setPageExpiredErrorPage(PageExpiredErrorPage.class); + + acceptedOrigins = StringUtils.split(getConfigurationParameter(ACCEPTED_ORIGIN_WHITELIST, null), " ,\t\f\r\n"); try { String cfgParam = getConfigurationParameter(MAXUPLOAD_PARAM, null); if (cfgParam != null && cfgParam.trim().length() > 0) { @@ -398,6 +406,11 @@ DEFAULT_DEVELOPMENT_REQUEST_TIMEOUT_MS); } @Override + public RequestCycle newRequestCycle(final Request request, final Response response) { + return new CsrfPreventionWebRequestCycle(this, (WebRequest)request, response, acceptedOrigins); + } + + @Override protected IRequestCycleProcessor newRequestCycleProcessor() { return new PluginRequestCycleProcessor(); } ===================================== engine/src/main/java/org/hippoecm/frontend/http/CsrfPreventionWebRequestCycle.java ===================================== --- /dev/null +++ b/engine/src/main/java/org/hippoecm/frontend/http/CsrfPreventionWebRequestCycle.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Original fork of wicket's 6.21.0 CsrfPreventionRequestCycleListener, + * see https://github.com/apache/wicket/blob/build/wicket-6.21.0/wicket-core/src/main/java/org/apache/wicket/protocol/http/CsrfPreventionRequestCycleListener.java. + * This class has been forked and modified to support a CsrfPreventionWebRequestCycle instead of a RequestCycleListener because + * the RequestCycleListener concept was not yet introduced in wicket 1.4.x. Also this makes sure it works behind proxies like httpd + */ +package org.hippoecm.frontend.http; + + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.wicket.Response; +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.protocol.http.WebRequest; +import org.apache.wicket.protocol.http.WebRequestCycle; +import org.apache.wicket.protocol.http.servlet.AbortWithWebErrorCodeException; +import org.apache.wicket.util.string.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + + +public class CsrfPreventionWebRequestCycle extends WebRequestCycle { + + private static final Logger log = LoggerFactory.getLogger(CsrfPreventionWebRequestCycle.class); + + /** + * The error code to report when the action to take for a CSRF request is + */ + private final static int errorCode = javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; + + private final String[] acceptedOrigins; + + /** + * The error message to report when the action to take for a CSRF request is {@code ERROR}. + * Default {@code "Origin does not correspond to request"}. + */ + private String errorMessage = "Origin does not correspond to request"; + + public CsrfPreventionWebRequestCycle(final WebApplication application, + final WebRequest request, + final Response response, + final String[] acceptedOrigins) { + super(application, request, response); + this.acceptedOrigins = acceptedOrigins; + } + + @Override + protected void onBeginRequest() { + + final HttpServletRequest httpServletRequest = getWebRequest().getHttpServletRequest(); + final String origin = httpServletRequest.getHeader("origin"); + if (origin == null || origin.isEmpty()) { + super.onBeginRequest(); + return; + } + + if (isWhitelistedOrigin(origin)) { + log.info("Allow whitelisted origin '{}'", origin); + return; + } + + if (!isLocalOrigin(httpServletRequest, origin)) { + log.info("Possible CSRF attack, request URL: {}, Origin: {}, action: aborted with error {} {}", + new Object[] { httpServletRequest.getRequestURL(), origin, errorCode, errorMessage }); + throw new AbortWithWebErrorCodeException(errorCode, errorMessage); + } + super.onBeginRequest(); + } + + private boolean isWhitelistedOrigin(final String origin) { + if (acceptedOrigins == null || acceptedOrigins.length == 0) { + return false; + } + + try + { + final URI originUri = new URI(origin); + final String originHost = originUri.getHost(); + if (Strings.isEmpty(originHost)) + return false; + for (String whitelistedOrigin : acceptedOrigins) + { + if (originHost.equalsIgnoreCase(whitelistedOrigin) || + originHost.endsWith("." + whitelistedOrigin)) + { + log.trace("Origin {} matched whitelisted origin {}, request accepted", origin, + whitelistedOrigin); + return true; + } + } + } + catch (URISyntaxException e) + { + log.debug("Origin: {} not parseable as an URI. Whitelisted-origin check skipped.", + origin); + } + + return false; + } + + /** + * Checks whether the {@code Origin} HTTP header of the request matches where the request came + * from. + * + * @param containerRequest the current container request + * @param originHeader the contents of the {@code Origin} HTTP header + * @return {@code true} when the origin of the request matches the {@code Origin} HTTP header + */ + private boolean isLocalOrigin(final HttpServletRequest containerRequest, final String originHeader) { + // Make comparable strings from Origin and Location + String origin = getOriginHeaderOrigin(originHeader); + if (origin == null) { + return false; + } + + String request = getLocationHeaderOrigin(containerRequest); + if (request == null) { + return false; + } + + return origin.equalsIgnoreCase(request); + } + + /** + * Creates a RFC-6454 comparable origin from the {@code origin} string. + * + * @param origin the contents of the Origin HTTP header + * @return only the scheme://host[:port] part, or {@code null} when the origin string is not + * compliant + */ + private String getOriginHeaderOrigin(final String origin) { + // the request comes from a privacy sensitive context, flag as non-local origin. If + // alternative action is required, an implementor can override any of the onAborted, + // onSuppressed or onAllowed and implement such needed action. + + if ("null".equals(origin)) { + return null; + } + + StringBuilder target = new StringBuilder(); + + try { + URI originUri = new URI(origin); + String scheme = originUri.getScheme(); + if (scheme == null) { + return null; + } else { + scheme = scheme.toLowerCase(Locale.ENGLISH); + } + + target.append(scheme); + target.append("://"); + + String host = originUri.getHost(); + if (host == null) { + return null; + } + target.append(host); + + int port = originUri.getPort(); + boolean portIsSpecified = port != -1; + boolean isAlternateHttpPort = "http".equals(scheme) && port != 80; + boolean isAlternateHttpsPort = "https".equals(scheme) && port != 443; + + if (portIsSpecified && (isAlternateHttpPort || isAlternateHttpsPort)) { + target.append(':'); + target.append(port); + } + return target.toString(); + } catch (URISyntaxException e) { + log.debug("Invalid Origin header provided: {}, marked conflicting", origin); + return null; + } + } + + /** + * Creates a RFC-6454 comparable origin from the {@code request} requested resource. + * + * @param request the incoming request + * @return only the scheme://host[:port] part, or {@code null} when the origin string is not + * compliant + */ + private String getLocationHeaderOrigin(final HttpServletRequest request) { + + String host = request.getHeader("X-Forwarded-Host"); + if (host != null) { + String[] hosts = host.split(","); + return getFarthestRequestScheme(request) + "://" + hosts[0]; + } + + host = request.getHeader("Host"); + if (host != null && !"".equals(host)) { + return getFarthestRequestScheme(request) + "://" + host; + } + + // Build scheme://host:port from request + StringBuilder target = new StringBuilder(); + String scheme = request.getScheme(); + if (scheme == null) { + return null; + } else { + scheme = scheme.toLowerCase(Locale.ENGLISH); + } + target.append(scheme); + target.append("://"); + + host = request.getServerName(); + if (host == null) { + return null; + } + target.append(host); + + int port = request.getServerPort(); + if ("http".equals(scheme) && port != 80 || "https".equals(scheme) && port != 443) { + target.append(':'); + target.append(port); + } + + return target.toString(); + } + + public static String getFarthestRequestScheme(final HttpServletRequest request) { + String [] schemes = getCommaSeparatedMultipleHeaderValues(request, "X-Forwarded-Proto"); + + if (schemes != null && schemes.length != 0) { + return schemes[0].toLowerCase(); + } + + schemes = getCommaSeparatedMultipleHeaderValues(request, "X-Forwarded-Scheme"); + + if (schemes != null && schemes.length != 0) { + return schemes[0].toLowerCase(); + } + + String [] sslEnabledArray = getCommaSeparatedMultipleHeaderValues(request, "X-SSL-Enabled"); + + if (sslEnabledArray == null) { + sslEnabledArray = getCommaSeparatedMultipleHeaderValues(request, "Front-End-Https"); + } + + if (sslEnabledArray != null && sslEnabledArray.length != 0) { + String sslEnabled = sslEnabledArray[0]; + + if (sslEnabled.equalsIgnoreCase("on") || sslEnabled.equalsIgnoreCase("yes") || sslEnabled.equals("1")) { + return "https"; + } + } + + return request.getScheme(); + } + + + /** + * Parse comma separated multiple header value and return an array if the header exists. + * If the header doesn't exist, it returns null. + * @param request + * @param headerName + * @return null if the header doesn't exist or an array parsed from the comma separated string header value. + */ + private static String [] getCommaSeparatedMultipleHeaderValues(final HttpServletRequest request, final String headerName) { + String value = request.getHeader(headerName); + + if (value == null) { + return null; + } + + String [] tokens = value.split(","); + + for (int i = 0; i < tokens.length; i++) { + tokens[i] = tokens[i].trim(); + } + + return tokens; + } + +} ===================================== pom.xml ===================================== --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - Copyright 2007-2015 Hippo B.V. (http://www.onehippo.com) + Copyright 2007-2016 Hippo B.V. (http://www.onehippo.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -639,6 +639,7 @@ <exclude>api/src/main/java/org/hippoecm/frontend/widgets/res/tree.css</exclude> <!-- forked from Apache Tomcat sources --> <exclude>engine/src/main/java/org/hippoecm/frontend/custom/ServerCookie.java</exclude> + <exclude>engine/src/main/java/org/hippoecm/frontend/http/CsrfPreventionWebRequestCycle.java</exclude> <!-- external contributions --> <exclude>gotolink/src/test/java/org/hippoecm/frontend/plugins/gotolink/GotolinkDocumentsShortcutPluginTest.java</exclude> <exclude>gotolink/src/test/java/org/hippoecm/frontend/plugins/gotolink/MyHomePage.java</exclude> View it on GitLab: https://code.onehippo.org/cms-community/hippo-cms/compare/8a883797c1693405c28972e9b8ad898a1e99b262...d54f5eb260f9c3e48db40ed98c61b71b17511326
_______________________________________________ Hippocms-svn mailing list Hippocms-svn@lists.onehippo.org https://lists.onehippo.org/mailman/listinfo/hippocms-svn