This is an automated email from the ASF dual-hosted git repository.

papegaaij pushed a commit to branch csp
in repository https://gitbox.apache.org/repos/asf/wicket.git


The following commit(s) were added to refs/heads/csp by this push:
     new 01f0450  WICKET-6727: first code drop for configurable CSP
01f0450 is described below

commit 01f0450b4f1e43898e5342174d8225bf08a9d373
Author: Emond Papegaaij <[email protected]>
AuthorDate: Mon Jan 13 22:04:00 2020 +0100

    WICKET-6727: first code drop for configurable CSP
---
 .../wicket/csp/CSPSettingRequestCycleListener.java | 535 +++++++++++++++++++++
 .../csp/CspNonceHeaderResponseDecorator.java       |  56 +++
 .../csp/CSPSettingRequestCycleListenerTest.java    | 448 +++++++++++++++++
 3 files changed, 1039 insertions(+)

diff --git 
a/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java
 
b/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java
new file mode 100644
index 0000000..a32af03
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java
@@ -0,0 +1,535 @@
+package org.apache.wicket.csp;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.cycle.IRequestCycleListener;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.http.WebResponse;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * An {@link IRequestCycleListener} that adds {@code Content-Security-Policy} 
and/or
+ * {@code Content-Security-Policy-Report-Only} headers based on the supplied 
configuration.
+ *
+ * See also the {@code CSPSettingRequestCycleListenerTest}.
+ *
+ * Example usage:
+ *
+ * <pre>
+ * {@code
+ *      myApplication.getRequestCycleListeners().add(
+ *                     new CSPSettingRequestCycleListener()
+ *                             .addBlockingDirective(CSPDirective.DEFAULT_SRC, 
CSPDirectiveSrcValue.NONE)
+ *                             .addBlockingDirective(CSPDirective.SCRIPT_SRC, 
CSPDirectiveSrcValue.SELF)
+ *                             .addBlockingDirective(CSPDirective.IMG_SRC, 
CSPDirectiveSrcValue.SELF)
+ *                             .addBlockingDirective(CSPDirective.FONT_SRC, 
CSPDirectiveSrcValue.SELF));
+ *
+ *              myApplication.getRequestCycleListeners().add(
+ *                     new CSPSettingRequestCycleListener()
+ *                             
.addReportingDirective(CSPDirective.DEFAULT_SRC, CSPDirectiveSrcValue.NONE)
+ *                             .addReportingDirective(CSPDirective.IMG_SRC, 
CSPDirectiveSrcValue.SELF)
+ *                             .addReportingDirective(CSPDirective.FONT_SRC, 
CSPDirectiveSrcValue.SELF)
+ *                             .addReportingDirective(CSPDirective.SCRIPT_SRC, 
CSPDirectiveSrcValue.SELF));
+ *     }
+ * </pre>
+ *
+ * {@code frame-src} has been deprecated since CSP 2.0 and replaced by {@code 
child-src}. Some
+ * browsers do not yet support {@code child-src} and expect {@code frame-src} 
instead. When
+ * {@code child-src} is added, a matching {@code frame-src} is added 
automatically for
+ * compatibility.
+ *
+ * @see "http://www.w3.org/TR/CSP2/";
+ * @see 
"https://developer.mozilla.org/en-US/docs/Web/Security/CSP/CSP_policy_directives";
+ *
+ * @author Sven Haster
+ * @author Emond Papegaaij
+ */
+public class CSPSettingRequestCycleListener implements IRequestCycleListener
+{
+       public static MetaDataKey<String> NONCE_KEY = new MetaDataKey<>()
+       {
+               private static final long serialVersionUID = 1L;
+       };
+
+       public static interface CSPRenderable
+       {
+               public String render(CSPSettingRequestCycleListener listener, 
RequestCycle cycle);
+       }
+
+       private static final class FixedCSPDirective implements CSPRenderable
+       {
+               private String value;
+
+               public FixedCSPDirective(String value)
+               {
+                       if (Strings.isEmpty(value))
+                               throw new IllegalArgumentException(
+                                       "CSP directive cannot have empty or 
null values");
+                       this.value = value;
+               }
+
+               @Override
+               public String render(CSPSettingRequestCycleListener listener, 
RequestCycle cycle)
+               {
+                       return value;
+               }
+       }
+
+       /**
+        * An enum holding the default values for -src directives including the 
mandatory single quotes
+        */
+       public enum CSPDirectiveSrcValue implements CSPRenderable
+       {
+               NONE("'none'"),
+               WILDCARD("*"),
+               SELF("'self'"),
+               UNSAFE_INLINE("'unsafe-inline'"),
+               UNSAFE_EVAL("'unsafe-eval'"),
+               STRICT_DYNAMIC("'strict-dynamic'"),
+               NONCE("'nonce-%1$s'")
+               {
+                       @Override
+                       public String render(CSPSettingRequestCycleListener 
listener, RequestCycle cycle)
+                       {
+                               return String.format(getValue(), 
listener.getNonce(cycle));
+                       }
+               };
+
+               private String value;
+
+               private CSPDirectiveSrcValue(String value)
+               {
+                       this.value = value;
+               }
+
+               @Override
+               public String render(CSPSettingRequestCycleListener listener, 
RequestCycle cycle)
+               {
+                       return value;
+               }
+
+               public String getValue()
+               {
+                       return value;
+               }
+       }
+
+       /**
+        * An enum representing the only possible values for the sandbox 
directive
+        */
+       public enum CSPDirectiveSandboxValue implements CSPRenderable
+       {
+               ALLOW_FORMS("allow-forms"),
+               ALLOW_SAME_ORIGIN("allow-same-origin"),
+               ALLOW_SCRIPTS("allow-scripts"),
+               ALLOW_TOP_NAVIGATION("allow-top-navigation"),
+               EMPTY("");
+
+               private String value;
+
+               private CSPDirectiveSandboxValue(String value)
+               {
+                       this.value = value;
+               }
+
+               public String getValue()
+               {
+                       return value;
+               }
+
+               @Override
+               public String render(CSPSettingRequestCycleListener listener, 
RequestCycle cycle)
+               {
+                       return value;
+               }
+       }
+
+       /** An enum holding the possible CSP Directives */
+       public enum CSPDirective
+       {
+               DEFAULT_SRC("default-src"),
+               SCRIPT_SRC("script-src"),
+               STYLE_SRC("style-src"),
+               IMG_SRC("img-src"),
+               CONNECT_SRC("connect-src"),
+               FONT_SRC("font-src"),
+               OBJECT_SRC("object-src"),
+               MANIFEST_SRC("manifest-src"),
+               MEDIA_SRC("media-src"),
+               CHILD_SRC("child-src"),
+               FRAME_ANCESTORS("frame-ancestors"),
+               @Deprecated
+               /** @deprecated Gebruik CHILD-SRC, deze zet ook automatisch 
FRAME-SRC. */
+               FRAME_SRC("frame-src"),
+               SANDBOX("sandbox")
+               {
+                       @Override
+                       protected void checkValueForDirective(CSPRenderable 
value,
+                                       List<CSPRenderable> 
existingDirectiveValues)
+                       {
+                               if (!existingDirectiveValues.isEmpty())
+                               {
+                                       if 
(CSPDirectiveSandboxValue.EMPTY.equals(value))
+                                       {
+                                               throw new 
IllegalArgumentException(
+                                                       "A sandbox directive 
can't contain an empty string if it already contains other values ");
+                                       }
+                                       if 
(existingDirectiveValues.contains(CSPDirectiveSandboxValue.EMPTY))
+                                       {
+                                               throw new 
IllegalArgumentException(
+                                                       "A sandbox directive 
can't contain other values if it already contains an empty string");
+                                       }
+                               }
+
+                               if (!(value instanceof 
CSPDirectiveSandboxValue))
+                               {
+                                       throw new IllegalArgumentException(
+                                               "A sandbox directive can only 
contain values from CSPDirectiveSandboxValue or be empty");
+                               }
+                       }
+               },
+               REPORT_URI("report-uri")
+               {
+                       @Override
+                       protected void checkValueForDirective(CSPRenderable 
value,
+                                       List<CSPRenderable> 
existingDirectiveValues)
+                       {
+                               if (!existingDirectiveValues.isEmpty())
+                               {
+                                       throw new IllegalArgumentException(
+                                               "A report-uri directive can 
only contain one uri");
+                               }
+                               if (!(value instanceof FixedCSPDirective))
+                               {
+                                       throw new IllegalArgumentException(
+                                               "A report-uri directive can 
only contain an URI");
+                               }
+                               try
+                               {
+                                       new URI(value.render(null, null));
+                               }
+                               catch (URISyntaxException urise)
+                               {
+                                       throw new 
IllegalArgumentException("Illegal URI for report-uri directive",
+                                               urise);
+                               }
+                       }
+               };
+
+               private String value;
+
+               private CSPDirective(String value)
+               {
+                       this.value = value;
+               }
+
+               public String getValue()
+               {
+                       return value;
+               }
+
+               protected void checkValueForDirective(CSPRenderable value,
+                               List<CSPRenderable> existingDirectiveValues)
+               {
+                       if (!existingDirectiveValues.isEmpty())
+                       {
+                               if (CSPDirectiveSrcValue.WILDCARD.equals(value)
+                                       || 
CSPDirectiveSrcValue.NONE.equals(value))
+                               {
+                                       throw new IllegalArgumentException(
+                                               "A -src directive can't contain 
an * or a 'none' if it already contains other values ");
+                               }
+                               if 
(existingDirectiveValues.contains(CSPDirectiveSrcValue.WILDCARD)
+                                       || 
existingDirectiveValues.contains(CSPDirectiveSrcValue.NONE))
+                               {
+                                       throw new IllegalArgumentException(
+                                               "A -src directive can't contain 
other values if it already contains an * or a 'none'");
+                               }
+                       }
+
+                       if (value instanceof CSPDirectiveSrcValue)
+                       {
+                               return;
+                       }
+
+                       if (value instanceof CSPDirectiveSandboxValue)
+                       {
+                               throw new IllegalArgumentException(
+                                       "A -src directive can't contain any of 
the sandbox directive values");
+                       }
+
+                       String strValue = value.render(null, null);
+                       if ("data:".equals(strValue) || 
"https:".equals(strValue))
+                       {
+                               return;
+                       }
+
+                       // strip off "*." so "*.example.com" becomes 
"example.com" and we can check if
+                       // it
+                       // is a valid uri
+                       if (strValue.startsWith("*."))
+                       {
+                               strValue = strValue.substring(2);
+                       }
+
+                       try
+                       {
+                               new URI(strValue);
+                       }
+                       catch (URISyntaxException urise)
+                       {
+                               throw new IllegalArgumentException("Illegal URI 
for -src directive", urise);
+                       }
+               }
+
+               /**
+                * @return The CSPDirective constant whose value-parameter 
equals the input-parameter or
+                *         {@code null} if none can be found.
+                */
+               public static CSPDirective fromValue(String value)
+               {
+                       if (Strings.isEmpty(value))
+                               return null;
+                       for (int i = 0; i < values().length; i++)
+                       {
+                               if (value.equals(values()[i].getValue()))
+                                       return values()[i];
+                       }
+                       return null;
+               }
+       }
+
+       private enum CSPHeaderMode
+       {
+               BLOCKING,
+               REPORT_ONLY;
+       }
+
+       private static String HEADER_CSP = "Content-Security-Policy";
+
+       private static String HEADER_CSP_REPORT = 
"Content-Security-Policy-Report-Only";
+
+       private static String HEADER_CSP_IE = "X-Content-Security-Policy";
+
+       private static String HEADER_CSP_REPORT_IE = 
"X-Content-Security-Policy-Report-Only";
+
+       // Directives for the 'Content-Security-Policy' header
+       private Map<CSPDirective, List<CSPRenderable>> blockingDirectives =
+               new EnumMap<>(CSPDirective.class);
+
+       // Directives for the 'Content-Security-Policy-Report-Only' header
+       private Map<CSPDirective, List<CSPRenderable>> reportingDirectives =
+               new EnumMap<>(CSPDirective.class);
+
+       private Function<Integer, byte[]> randomSupplier;
+
+       private boolean addLegacyHeaders = false;
+
+       public CSPSettingRequestCycleListener()
+       {
+       }
+
+       public CSPSettingRequestCycleListener(Function<Integer, byte[]> 
randomSupplier)
+       {
+               this.randomSupplier = randomSupplier;
+       }
+
+       /**
+        * True when legacy headers should be added.
+        * 
+        * @return True when legacy headers should be added.
+        */
+       public boolean isAddLegacyHeaders()
+       {
+               return addLegacyHeaders;
+       }
+
+       /**
+        * Enable legacy {@code X-Content-Security-Policy} headers for older 
browsers, such as IE.
+        * 
+        * @param addLegacyHeaders True when the legacy headers should be added.
+        * @return {@code this} for chaining
+        */
+       public CSPSettingRequestCycleListener setAddLegacyHeaders(boolean 
addLegacyHeaders)
+       {
+               this.addLegacyHeaders = addLegacyHeaders;
+               return this;
+       }
+
+       /**
+        * Adds any of the default values to a -src directive for the 
'blocking' CSP header
+        */
+       public CSPSettingRequestCycleListener addBlockingDirective(CSPDirective 
directive,
+                       CSPDirectiveSrcValue... values)
+       {
+               for (CSPDirectiveSrcValue value : values)
+               {
+                       addDirective(directive, value, CSPHeaderMode.BLOCKING);
+               }
+               return this;
+       }
+
+       /**
+        * Adds any of the default values to the sandbox directive for the 
'blocking' CSP header. Use
+        * {@link #addBlockingDirective(CSPDirective, String...)} with the 
sandbox {@link CSPDirective}
+        * and a single empty string (<em>not</em> {@code null}) to add the 
empty sandbox directive.
+        */
+       public CSPSettingRequestCycleListener addBlockingDirective(CSPDirective 
sandboxDirective,
+                       CSPDirectiveSandboxValue... values)
+       {
+               for (CSPDirectiveSandboxValue value : values)
+               {
+                       addDirective(sandboxDirective, value, 
CSPHeaderMode.BLOCKING);
+               }
+               return this;
+       }
+
+       /**
+        * Adds any value to a directive for the 'blocking' CSP header. Use
+        * {@link #addBlockingDirective(CSPDirective, 
CSPDirectiveSandboxValue...)} and
+        * {@link #addBlockingDirective(CSPDirective, CSPDirectiveSrcValue...)} 
for the default values
+        * for the sandbox and -src directives.
+        */
+       public CSPSettingRequestCycleListener addBlockingDirective(CSPDirective 
directive,
+                       String... values)
+       {
+               for (String value : values)
+               {
+                       addDirective(directive, new FixedCSPDirective(value), 
CSPHeaderMode.BLOCKING);
+               }
+               return this;
+       }
+
+       /**
+        * Adds any of the default values to a -src directive for the 
'reporting-only' CSP header
+        */
+       public CSPSettingRequestCycleListener 
addReportingDirective(CSPDirective directive,
+                       CSPDirectiveSrcValue... values)
+       {
+               for (CSPDirectiveSrcValue value : values)
+               {
+                       addDirective(directive, value, 
CSPHeaderMode.REPORT_ONLY);
+               }
+               return this;
+       }
+
+       /**
+        * Adds any of the default values to the sandbox directive for the 
'reporting-only' CSP header.
+        * Use {@link #addReportingDirective(CSPDirective, String...)} with the 
sandbox
+        * {@link CSPDirective} and a single empty string (<em>not</em> {@code 
null}) to add the empty
+        * sandbox directive.
+        */
+       public CSPSettingRequestCycleListener 
addReportingDirective(CSPDirective sandboxDirective,
+                       CSPDirectiveSandboxValue... values)
+       {
+               for (CSPDirectiveSandboxValue value : values)
+               {
+                       addDirective(sandboxDirective, value, 
CSPHeaderMode.REPORT_ONLY);
+               }
+               return this;
+       }
+
+       /**
+        * Adds any value to a directive for the 'reporting-only' CSP header. 
Use
+        * {@link #addReportingDirective(CSPDirective, 
CSPDirectiveSandboxValue...)} and
+        * {@link #addReportingDirective(CSPDirective, 
CSPDirectiveSrcValue...)} for the default values
+        * for the sandbox and -src directives.
+        */
+       public CSPSettingRequestCycleListener 
addReportingDirective(CSPDirective directive,
+                       String... values)
+       {
+               for (String value : values)
+               {
+                       addDirective(directive, new FixedCSPDirective(value), 
CSPHeaderMode.REPORT_ONLY);
+               }
+               return this;
+       }
+
+       private CSPSettingRequestCycleListener addDirective(CSPDirective 
directive, CSPRenderable value,
+                       CSPHeaderMode mode)
+       {
+               // Add backwards compatible frame-src
+               // see http://caniuse.com/#feat=contentsecuritypolicy2
+               if (CSPDirective.CHILD_SRC.equals(directive))
+               {
+                       addDirective(CSPDirective.FRAME_SRC, value, mode);
+               }
+               switch (mode)
+               {
+                       case BLOCKING:
+                               if (blockingDirectives.get(directive) == null)
+                               {
+                                       blockingDirectives.put(directive, new 
ArrayList<>());
+                               }
+                               directive.checkValueForDirective(value, 
blockingDirectives.get(directive));
+                               blockingDirectives.get(directive).add(value);
+                               return this;
+                       case REPORT_ONLY:
+                               if (reportingDirectives.get(directive) == null)
+                               {
+                                       reportingDirectives.put(directive, new 
ArrayList<>());
+                               }
+                               directive.checkValueForDirective(value, 
reportingDirectives.get(directive));
+                               reportingDirectives.get(directive).add(value);
+                               return this;
+                       default:
+                               throw new IllegalArgumentException("Incorrect 
CSPHeaderMode!");
+               }
+
+       }
+
+       @Override
+       public void onRequestHandlerResolved(RequestCycle cycle, 
IRequestHandler handler)
+       {
+               WebResponse webResponse = (WebResponse) cycle.getResponse();
+               if (!reportingDirectives.isEmpty())
+               {
+                       String reportHeaderValue = 
getCSPHeaderValue(reportingDirectives, cycle);
+                       webResponse.setHeader(HEADER_CSP_REPORT, 
reportHeaderValue);
+                       if (addLegacyHeaders)
+                               webResponse.setHeader(HEADER_CSP_REPORT_IE, 
reportHeaderValue);
+               }
+               if (!blockingDirectives.isEmpty())
+               {
+                       String blockHeaderValue = 
getCSPHeaderValue(blockingDirectives, cycle);
+                       webResponse.setHeader(HEADER_CSP, blockHeaderValue);
+                       if (addLegacyHeaders)
+                               webResponse.setHeader(HEADER_CSP_IE, 
blockHeaderValue);
+               }
+       }
+
+       public String getNonce(RequestCycle cycle)
+       {
+               String nonce = cycle.getMetaData(NONCE_KEY);
+               if (nonce == null)
+               {
+                       nonce = 
Base64.getEncoder().encodeToString(randomSupplier.apply(12));
+                       cycle.setMetaData(NONCE_KEY, nonce);
+               }
+               return nonce;
+       }
+
+       // @returns "key1 value1a value1b; key2 value2a; key3 value3a value3b 
value3c"
+       private String getCSPHeaderValue(Map<CSPDirective, List<CSPRenderable>> 
directiveValuesMap,
+                       RequestCycle cycle)
+       {
+               return directiveValuesMap.entrySet()
+                       .stream()
+                       .map(e -> e.getKey().getValue() + " "
+                               + e.getValue()
+                                       .stream()
+                                       .map(r -> r.render(this, cycle))
+                                       .collect(Collectors.joining(" ")))
+                       .collect(Collectors.joining("; "));
+       }
+}
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/csp/CspNonceHeaderResponseDecorator.java
 
b/wicket-core/src/main/java/org/apache/wicket/csp/CspNonceHeaderResponseDecorator.java
new file mode 100644
index 0000000..5aabcda
--- /dev/null
+++ 
b/wicket-core/src/main/java/org/apache/wicket/csp/CspNonceHeaderResponseDecorator.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.csp;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.markup.head.AbstractCspHeaderItem;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.IWrappedHeaderItem;
+import org.apache.wicket.markup.head.ResourceAggregator;
+import org.apache.wicket.markup.html.DecoratingHeaderResponse;
+import org.apache.wicket.request.cycle.RequestCycle;
+
+/**
+ * Add a <em>Content Security Policy<em> (CSP) nonce to all {@link 
AbstractCspHeaderItem}s.
+ * <p>
+ * Note: please don't forget to wrap with {@link ResourceAggregator} when 
setting it up with
+ * {@link Application#setHeaderResponseDecorator}, otherwise dependencies will 
not be rendered.
+ *
+ * @see AbstractCspHeaderItem
+ */
+public class CspNonceHeaderResponseDecorator extends DecoratingHeaderResponse
+{
+       private CSPSettingRequestCycleListener listener;
+
+       public CspNonceHeaderResponseDecorator(IHeaderResponse real, 
CSPSettingRequestCycleListener listener)
+       {
+               super(real);
+
+               this.listener = listener;
+       }
+
+       @Override
+       public void render(HeaderItem item)
+       {
+               while (item instanceof IWrappedHeaderItem)
+               {
+                       item = ((IWrappedHeaderItem) item).getWrapped();
+               }
+
+               if (item instanceof AbstractCspHeaderItem)
+               {
+                       ((AbstractCspHeaderItem) 
item).setNonce(listener.getNonce(RequestCycle.get()));
+               }
+
+               super.render(item);
+       }
+}
diff --git 
a/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
 
b/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
new file mode 100644
index 0000000..8e00759
--- /dev/null
+++ 
b/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
@@ -0,0 +1,448 @@
+package org.apache.wicket.csp;
+
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.CHILD_SRC;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.DEFAULT_SRC;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.FRAME_SRC;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.REPORT_URI;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.SANDBOX;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSandboxValue.ALLOW_FORMS;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSandboxValue.EMPTY;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue.NONE;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue.SELF;
+import static 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue.WILDCARD;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective;
+import 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSandboxValue;
+import 
org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue;
+import org.apache.wicket.mock.MockHomePage;
+import org.apache.wicket.util.tester.WicketTester;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@SuppressWarnings("deprecation")
+public class CSPSettingRequestCycleListenerTest
+{
+       private static String HEADER_CSP = "Content-Security-Policy";
+
+       private static String HEADER_CSP_REPORT = 
"Content-Security-Policy-Report-Only";
+
+       private WicketTester wicketTester;
+
+       @BeforeEach
+       public void setUp()
+       {
+               wicketTester = new WicketTester(MockHomePage.class);
+       }
+
+       @Test
+       public void testNullSrcInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, (String) 
null);
+               });
+       }
+
+       @Test
+       public void testEmptySrcInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, "");
+               });
+       }
+
+       /**
+        * A value for any of the -src directives can be a number of predefined 
values (for most of them
+        * you can use {@link CSPDirectiveSrcValue}) or a correct URI.
+        */
+       @Test
+       public void testInvalidSrcInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, 
"abc?^()-_\'xyz");
+               });
+       }
+
+       /**
+        * If {@code 'none'} is used for any of the -src directives, it must be 
the only value for that
+        * directive.
+        */
+       @Test
+       public void testMultipleSrcInputWithNoneIsRejected1()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, SELF, 
NONE);
+               });
+       }
+
+       /**
+        * If {@code 'none'} is used for any of the -src directives, it must be 
the only value for that
+        * directive.
+        */
+       @Test
+       public void testMultipleSrcInputWithNoneIsRejected2()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, NONE, 
SELF);
+               });
+       }
+
+       /**
+        * If {@code *} (asterisk) is used for any of the -src directives, it 
must be the only value for
+        * that directive.
+        */
+       @Test
+       public void testMultipleSrcInputWithStarIsRejected1()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               cspListener.addBlockingDirective(DEFAULT_SRC, SELF);
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, WILDCARD);
+               });
+       }
+
+       /**
+        * If {@code *} (asterisk) is used for any of the -src directives, it 
must be the only value for
+        * that directive.
+        */
+       @Test
+       public void testMultipleSrcInputWithStarIsRejected2()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               cspListener.addBlockingDirective(DEFAULT_SRC, WILDCARD);
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, SELF);
+               });
+       }
+
+       @Test
+       public void testWrongSrcInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(DEFAULT_SRC, 
ALLOW_FORMS);
+               });
+       }
+
+       @Test
+       public void testWrongSandboxInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(SANDBOX, SELF);
+               });
+       }
+
+       @Test
+       public void testNullSandboxInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(SANDBOX, (String) 
null);
+               });
+       }
+
+       @Test
+       public void testEmptySandboxInputIsAccepted()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               cspListener.addBlockingDirective(SANDBOX, 
CSPDirectiveSandboxValue.EMPTY);
+       }
+
+       @Test
+       public void testInvalidSandboxInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(SANDBOX, "abcxyz");
+               });
+       }
+
+       @Test
+       public void testMultipleSandboxInputWithEmptyStringIsRejected1()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               cspListener.addBlockingDirective(SANDBOX, ALLOW_FORMS);
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(SANDBOX, EMPTY);
+               });
+       }
+
+       @Test
+       public void testMultipleSandboxInputWithEmptyStringIsRejected2()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               cspListener.addBlockingDirective(SANDBOX, EMPTY);
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(SANDBOX, ALLOW_FORMS);
+               });
+       }
+
+       @Test
+       public void testNullReportUriInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(REPORT_URI, (String) 
null);
+               });
+       }
+
+       @Test
+       public void testEmptyReportUriInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(REPORT_URI, "");
+               });
+       }
+
+       @Test
+       public void testInvalidReportUriInputIsRejected()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               Assertions.assertThrows(IllegalArgumentException.class, () -> {
+                       cspListener.addBlockingDirective(REPORT_URI, 
"abc?^()-_\'xyz");
+               });
+       }
+
+       @Test
+       public void testAllCSPSrcDefaultEnumsAreSetCorrectly() throws 
NoSuchAlgorithmException
+       {
+               SecureRandom random = SecureRandom.getInstanceStrong();
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener(length -> {
+                       byte[] ret = new byte[length];
+                       random.nextBytes(ret);
+                       return ret;
+               });
+
+               final int cspDirectiveCount = CSPDirective.values().length;
+               final int cspDirectiveSrcValueCount = 
CSPDirectiveSrcValue.values().length;
+               for (int i = 0; i < Math.max(cspDirectiveCount, 
cspDirectiveSrcValueCount); i++)
+               {
+                       final CSPDirective cspDirective = 
CSPDirective.values()[i % cspDirectiveCount];
+                       // FRAME-SRC wordt al gezet door de aanroep voor 
CHILD-SRC
+                       if (!FRAME_SRC.equals(cspDirective) && 
cspDirective.getValue().endsWith("-src"))
+                       {
+                               final CSPDirectiveSrcValue cspDirectiveValue =
+                                       CSPDirectiveSrcValue.values()[i % 
cspDirectiveSrcValueCount];
+                               cspListener.addBlockingDirective(cspDirective, 
cspDirectiveValue);
+
+                               cspListener.addReportingDirective(cspDirective, 
cspDirectiveValue);
+                       }
+               }
+
+               StringBuffer headerErrors = checkHeaders(cspListener);
+
+               if (headerErrors.length() > 0)
+               {
+                       Assertions.fail(headerErrors.toString());
+               }
+       }
+
+       @Test
+       public void testCSPReportUriDirectiveSetCorrectly()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               cspListener.addBlockingDirective(REPORT_URI, 
"http://report.example.com";);
+               cspListener.addReportingDirective(REPORT_URI, 
"/example-report-uri");
+
+               StringBuffer headerErrors = checkHeaders(cspListener);
+
+               if (headerErrors.length() > 0)
+               {
+                       Assertions.fail(headerErrors.toString());
+               }
+       }
+
+       @Test
+       public void testCSPSandboxDirectiveSetCorrectly()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               final int cspSandboxDirectiveValueCount = 
CSPDirectiveSandboxValue.values().length;
+               for (int i = 0; i < cspSandboxDirectiveValueCount; i++)
+               {
+                       final CSPDirectiveSandboxValue cspDirectiveValue = 
CSPDirectiveSandboxValue.values()[i];
+                       if 
(cspDirectiveValue.equals(CSPDirectiveSandboxValue.EMPTY))
+                               continue;
+                       
+                       cspListener.addBlockingDirective(SANDBOX, 
cspDirectiveValue);
+                       cspListener.addReportingDirective(SANDBOX, 
cspDirectiveValue);
+               }
+
+               StringBuffer headerErrors = checkHeaders(cspListener);
+
+               if (headerErrors.length() > 0)
+               {
+                       Assertions.fail(headerErrors.toString());
+               }
+       }
+
+       // FF 36+, IE (incl. Edge), Safari en Opera Mini hebben nog geen 
(volledige)
+       // support voor CSP, wat betekent dat ze CHILD-SRC niet kennen en 
FRAME-SRC
+       // verwachten. Daarom in de CSPSettingRCL een hack om alle CHILD-SRC's 
die geset
+       // worden ook als FRAME-SRC te setten.
+       // Zie http://caniuse.com/#feat=contentsecuritypolicy2
+       @Test
+       public void testChildSrcDirectiveAlsoSetsFrameSrcDirective()
+       {
+               CSPSettingRequestCycleListener cspListener = new 
CSPSettingRequestCycleListener();
+               cspListener.addBlockingDirective(CHILD_SRC, SELF);
+               cspListener.addReportingDirective(CHILD_SRC, SELF);
+               StringBuffer headerErrors = checkHeaders(cspListener);
+
+               if (headerErrors.length() > 0)
+               {
+                       Assertions.fail(headerErrors.toString());
+               }
+       }
+
+       private StringBuffer checkHeaders(CSPSettingRequestCycleListener 
cspListener)
+       {
+               StringBuffer headerErrors = new StringBuffer();
+               wicketTester.getRequestCycle().getListeners().add(cspListener);
+               wicketTester.executeUrl("/");
+               String cspHeaderValue = 
wicketTester.getLastResponse().getHeader(HEADER_CSP);
+               String cspReportingHeaderValue =
+                       
wicketTester.getLastResponse().getHeader(HEADER_CSP_REPORT);
+
+               if (cspHeaderValue == null)
+               {
+                       headerErrors.append(
+                               String.format("Header %s expected but either 
not present or empty", HEADER_CSP));
+               }
+               if (cspReportingHeaderValue == null)
+               {
+                       headerErrors.append(String.format("Header %s expected 
but either not present or empty",
+                               HEADER_CSP_REPORT));
+               }
+
+               if (headerErrors.length() > 0)
+               {
+                       return headerErrors;
+               }
+
+               StringBuffer headerValueErrors = new StringBuffer();
+               List<String> blockingHeaderValueErrors = 
checkCSPHeaderValues(cspHeaderValue);
+               List<String> reportingHeaderValueErrors = 
checkCSPHeaderValues(cspReportingHeaderValue);
+
+               if (!blockingHeaderValueErrors.isEmpty())
+               {
+                       headerValueErrors.append("Blocking-mode CSP header 
value issues: ");
+                       headerValueErrors
+                               
.append(blockingHeaderValueErrors.stream().collect(Collectors.joining("; ")));
+                       headerValueErrors.append(". ");
+               }
+               if (!reportingHeaderValueErrors.isEmpty())
+               {
+                       headerValueErrors.append("Reporting-mode CSP header 
value issues: ");
+                       headerValueErrors
+                               
.append(reportingHeaderValueErrors.stream().collect(Collectors.joining("; ")));
+                       headerValueErrors.append(". ");
+               }
+               return headerValueErrors;
+       }
+
+       private List<String> checkCSPHeaderValues(String cspHeaderValue)
+       {
+               Set<String> directiveValues = Stream.of(CSPDirective.values())
+                       .map(CSPDirective::getValue)
+                       .collect(Collectors.toSet());
+               Set<String> directiveSrcValues = 
Stream.of(CSPDirectiveSrcValue.values())
+                       .map(CSPDirectiveSrcValue::getValue)
+                       .collect(Collectors.toSet());
+               Set<String> directiveSandboxValues = 
Stream.of(CSPDirectiveSandboxValue.values())
+                       .map(CSPDirectiveSandboxValue::getValue)
+                       .collect(Collectors.toSet());
+
+               final List<String> errors = new ArrayList<>();
+               String[] directives = cspHeaderValue.split(";");
+               boolean hasChildSrc = false, hasFrameSrc = false;
+               for (String directive : directives)
+               {
+                       directive = directive.trim();
+                       String[] values = directive.split("\\s");
+                       String directiveName = values[0];
+                       if (!directiveValues.contains(directiveName))
+                       {
+                               errors.add(
+                                       String.format("Directive %s is not a 
valid directive name", directiveName));
+                       }
+                       else
+                       {
+                               if 
(CSPDirective.fromValue(directiveName).equals(FRAME_SRC))
+                               {
+                                       hasFrameSrc = true;
+                               }
+                               if 
(CSPDirective.fromValue(directiveName).equals(CHILD_SRC))
+                               {
+                                       hasChildSrc = true;
+                               }
+                               for (int i = 1; i < values.length; i++)
+                               {
+                                       final String trimmedValue = 
values[i].trim();
+                                       final boolean isValidDefaultSrcValue =
+                                               
directiveSrcValues.contains(trimmedValue);
+                                       final boolean 
isValidDefaultSandboxValue =
+                                               
directiveSandboxValues.contains(trimmedValue);
+                                       if (!(isValidDefaultSrcValue || 
isValidDefaultSandboxValue
+                                               || 
isValidDirectiveValue(trimmedValue)))
+                                       {
+                                               errors.add(
+                                                       String.format("Value %s 
is not a valid directive value", trimmedValue));
+                                       }
+                               }
+                       }
+               }
+
+               if (hasFrameSrc != hasChildSrc)
+               {
+                       String presentDirective = hasFrameSrc ? 
FRAME_SRC.getValue() : CHILD_SRC.getValue();
+                       String notPresentDirective = !hasFrameSrc ? 
FRAME_SRC.getValue() : CHILD_SRC.getValue();
+                       errors.add(String.format("Directive %s present without 
directive %s for fallback",
+                               presentDirective, notPresentDirective));
+               }
+
+               return errors;
+       }
+
+       // @see: http://content-security-policy.com/#source_list
+       private boolean isValidDirectiveValue(String directiveValue)
+       {
+               if ("*".equals(directiveValue))
+                       return true;
+               else if ("data:".equals(directiveValue) || 
"https:".equals(directiveValue))
+                       return true;
+
+               // strip off "*." for "*.example.com" so we can check 
"example.com" to be a valid
+               // URI.
+               if (directiveValue.startsWith("*."))
+                       directiveValue = directiveValue.substring(2);
+               try
+               {
+                       new URI(directiveValue);
+                       return true;
+               }
+               catch (URISyntaxException ignored)
+               {
+                       // fall through
+               }
+
+               return false;
+       }
+
+}

Reply via email to