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

Reply via email to