You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by pa...@apache.org on 2020/01/17 21:26:56 UTC

[wicket] 01/08: WICKET-6727: first code drop for configurable CSP

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

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

commit 0a5cdf17b06b231074788608196c0c7b768f986f
Author: Emond Papegaaij <em...@topicus.nl>
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;
+	}
+
+}