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/16 08:27:48 UTC

[wicket] branch csp-configurable created (now ed5c301)

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

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


      at ed5c301  WICKET-6727: fix license headers

This branch includes the following new commits:

     new 1125c3d  WICKET-6727: first code drop for configurable CSP
     new 4a0bab6  WICKET-6727: refactoring of CSP API
     new ed5c301  WICKET-6727: fix license headers

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[wicket] 02/03: WICKET-6727: refactoring of CSP API

Posted by pa...@apache.org.
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 4a0bab696ac28cb155234c4e724b38f1ac6a8e24
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Wed Jan 15 11:20:47 2020 +0100

    WICKET-6727: refactoring of CSP API
---
 .../wicket/csp/CSPSettingRequestCycleListener.java | 300 +++++++++------------
 .../csp/CSPSettingRequestCycleListenerTest.java    |  58 ++--
 2 files changed, 160 insertions(+), 198 deletions(-)

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
index a32af03..47c6cdd 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java
@@ -5,6 +5,7 @@ import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.EnumMap;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
@@ -311,201 +312,176 @@ public class CSPSettingRequestCycleListener implements IRequestCycleListener
 
 	private enum CSPHeaderMode
 	{
-		BLOCKING,
-		REPORT_ONLY;
-	}
+		BLOCKING("Content-Security-Policy"),
+		REPORT_ONLY("Content-Security-Policy-Report-Only");
 
-	private static String HEADER_CSP = "Content-Security-Policy";
+		private final String header;
 
-	private static String HEADER_CSP_REPORT = "Content-Security-Policy-Report-Only";
+		private CSPHeaderMode(String header)
+		{
+			this.header = header;
+		}
 
-	private static String HEADER_CSP_IE = "X-Content-Security-Policy";
+		public String getHeader()
+		{
+			return header;
+		}
 
-	private static String HEADER_CSP_REPORT_IE = "X-Content-Security-Policy-Report-Only";
+		public String getLegacyHeader()
+		{
+			return "X-" + getHeader();
+		}
+	}
 
-	// Directives for the 'Content-Security-Policy' header
-	private Map<CSPDirective, List<CSPRenderable>> blockingDirectives =
-		new EnumMap<>(CSPDirective.class);
+	public class CSPConfiguration
+	{
+		private CSPHeaderMode mode;
 
-	// Directives for the 'Content-Security-Policy-Report-Only' header
-	private Map<CSPDirective, List<CSPRenderable>> reportingDirectives =
-		new EnumMap<>(CSPDirective.class);
+		private Map<CSPDirective, List<CSPRenderable>> directives =
+			new EnumMap<>(CSPDirective.class);
 
-	private Function<Integer, byte[]> randomSupplier;
+		private boolean addLegacyHeaders = false;
 
-	private boolean addLegacyHeaders = false;
+		private CSPConfiguration(CSPHeaderMode mode)
+		{
+			this.mode = mode;
+		}
 
-	public CSPSettingRequestCycleListener()
-	{
-	}
+		public CSPHeaderMode getMode()
+		{
+			return mode;
+		}
 
-	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;
+		}
 
-	/**
-	 * 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 CSPConfiguration setAddLegacyHeaders(boolean addLegacyHeaders)
+		{
+			this.addLegacyHeaders = addLegacyHeaders;
+			return this;
+		}
 
-	/**
-	 * 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;
-	}
+		public CSPConfiguration addDirective(CSPDirective directive, CSPDirectiveSrcValue... values)
+		{
+			for (CSPDirectiveSrcValue value : values)
+			{
+				doAddDirective(directive, value);
+			}
+			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)
+		/**
+		 * 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 CSPConfiguration addDirective(CSPDirective sandboxDirective,
+				CSPDirectiveSandboxValue... values)
 		{
-			addDirective(directive, value, CSPHeaderMode.BLOCKING);
+			for (CSPDirectiveSandboxValue value : values)
+			{
+				doAddDirective(sandboxDirective, value);
+			}
+			return this;
 		}
-		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)
+		/**
+		 * 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 CSPConfiguration addDirective(CSPDirective directive, String... values)
 		{
-			addDirective(sandboxDirective, value, CSPHeaderMode.BLOCKING);
+			for (String value : values)
+			{
+				doAddDirective(directive, new FixedCSPDirective(value));
+			}
+			return this;
 		}
-		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)
+		public boolean isSet()
 		{
-			addDirective(directive, new FixedCSPDirective(value), CSPHeaderMode.BLOCKING);
+			return !directives.isEmpty();
 		}
-		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)
+		private CSPConfiguration doAddDirective(CSPDirective directive, CSPRenderable value)
 		{
-			addDirective(directive, value, CSPHeaderMode.REPORT_ONLY);
+			// Add backwards compatible frame-src
+			// see http://caniuse.com/#feat=contentsecuritypolicy2
+			if (CSPDirective.CHILD_SRC.equals(directive))
+			{
+				doAddDirective(CSPDirective.FRAME_SRC, value);
+			}
+			List<CSPRenderable> values =
+				directives.computeIfAbsent(directive, x -> new ArrayList<>());
+			directive.checkValueForDirective(value, values);
+			values.add(value);
+			return this;
 		}
-		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)
+		// @returns "key1 value1a value1b; key2 value2a; key3 value3a value3b value3c"
+		public String renderHeaderValue(RequestCycle cycle)
 		{
-			addDirective(sandboxDirective, value, CSPHeaderMode.REPORT_ONLY);
+			return directives.entrySet()
+				.stream()
+				.map(e -> e.getKey().getValue() + " "
+					+ e.getValue()
+						.stream()
+						.map(r -> r.render(CSPSettingRequestCycleListener.this, cycle))
+						.collect(Collectors.joining(" ")))
+				.collect(Collectors.joining("; "));
 		}
-		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)
+	private Function<Integer, byte[]> randomSupplier;
+
+	private Map<CSPHeaderMode, CSPConfiguration> configs = new HashMap<>();
+
+	public CSPSettingRequestCycleListener()
 	{
-		for (String value : values)
-		{
-			addDirective(directive, new FixedCSPDirective(value), CSPHeaderMode.REPORT_ONLY);
-		}
-		return this;
 	}
 
-	private CSPSettingRequestCycleListener addDirective(CSPDirective directive, CSPRenderable value,
-			CSPHeaderMode mode)
+	public CSPSettingRequestCycleListener(Function<Integer, byte[]> randomSupplier)
 	{
-		// 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!");
-		}
+		this.randomSupplier = randomSupplier;
+	}
+
+	public CSPConfiguration blocking()
+	{
+		return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, CSPConfiguration::new);
+	}
 
+	public CSPConfiguration reporting()
+	{
+		return configs.computeIfAbsent(CSPHeaderMode.REPORT_ONLY, CSPConfiguration::new);
 	}
 
 	@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);
-		}
+		configs.values().stream().filter(CSPConfiguration::isSet).forEach(config -> {
+			String headerValue = config.renderHeaderValue(cycle);
+			webResponse.setHeader(config.getMode().getHeader(), headerValue);
+			if (config.isAddLegacyHeaders())
+				webResponse.setHeader(config.getMode().getLegacyHeader(), headerValue);
+		});
 	}
 
 	public String getNonce(RequestCycle cycle)
@@ -518,18 +494,4 @@ public class CSPSettingRequestCycleListener implements IRequestCycleListener
 		}
 		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/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java b/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
index 8e00759..f347f8a 100644
--- a/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
@@ -50,7 +50,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, (String) null);
+			cspListener.blocking().addDirective(DEFAULT_SRC, (String) null);
 		});
 	}
 
@@ -59,7 +59,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, "");
+			cspListener.blocking().addDirective(DEFAULT_SRC, "");
 		});
 	}
 
@@ -72,7 +72,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, "abc?^()-_\'xyz");
+			cspListener.blocking().addDirective(DEFAULT_SRC, "abc?^()-_\'xyz");
 		});
 	}
 
@@ -85,7 +85,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, SELF, NONE);
+			cspListener.blocking().addDirective(DEFAULT_SRC, SELF, NONE);
 		});
 	}
 
@@ -98,7 +98,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, NONE, SELF);
+			cspListener.blocking().addDirective(DEFAULT_SRC, NONE, SELF);
 		});
 	}
 
@@ -110,9 +110,9 @@ public class CSPSettingRequestCycleListenerTest
 	public void testMultipleSrcInputWithStarIsRejected1()
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
-		cspListener.addBlockingDirective(DEFAULT_SRC, SELF);
+		cspListener.blocking().addDirective(DEFAULT_SRC, SELF);
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, WILDCARD);
+			cspListener.blocking().addDirective(DEFAULT_SRC, WILDCARD);
 		});
 	}
 
@@ -124,9 +124,9 @@ public class CSPSettingRequestCycleListenerTest
 	public void testMultipleSrcInputWithStarIsRejected2()
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
-		cspListener.addBlockingDirective(DEFAULT_SRC, WILDCARD);
+		cspListener.blocking().addDirective(DEFAULT_SRC, WILDCARD);
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, SELF);
+			cspListener.blocking().addDirective(DEFAULT_SRC, SELF);
 		});
 	}
 
@@ -135,7 +135,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(DEFAULT_SRC, ALLOW_FORMS);
+			cspListener.blocking().addDirective(DEFAULT_SRC, ALLOW_FORMS);
 		});
 	}
 
@@ -144,7 +144,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(SANDBOX, SELF);
+			cspListener.blocking().addDirective(SANDBOX, SELF);
 		});
 	}
 
@@ -153,7 +153,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(SANDBOX, (String) null);
+			cspListener.blocking().addDirective(SANDBOX, (String) null);
 		});
 	}
 
@@ -161,7 +161,7 @@ public class CSPSettingRequestCycleListenerTest
 	public void testEmptySandboxInputIsAccepted()
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
-		cspListener.addBlockingDirective(SANDBOX, CSPDirectiveSandboxValue.EMPTY);
+		cspListener.blocking().addDirective(SANDBOX, CSPDirectiveSandboxValue.EMPTY);
 	}
 
 	@Test
@@ -169,7 +169,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(SANDBOX, "abcxyz");
+			cspListener.blocking().addDirective(SANDBOX, "abcxyz");
 		});
 	}
 
@@ -177,9 +177,9 @@ public class CSPSettingRequestCycleListenerTest
 	public void testMultipleSandboxInputWithEmptyStringIsRejected1()
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
-		cspListener.addBlockingDirective(SANDBOX, ALLOW_FORMS);
+		cspListener.blocking().addDirective(SANDBOX, ALLOW_FORMS);
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(SANDBOX, EMPTY);
+			cspListener.blocking().addDirective(SANDBOX, EMPTY);
 		});
 	}
 
@@ -187,9 +187,9 @@ public class CSPSettingRequestCycleListenerTest
 	public void testMultipleSandboxInputWithEmptyStringIsRejected2()
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
-		cspListener.addBlockingDirective(SANDBOX, EMPTY);
+		cspListener.blocking().addDirective(SANDBOX, EMPTY);
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(SANDBOX, ALLOW_FORMS);
+			cspListener.blocking().addDirective(SANDBOX, ALLOW_FORMS);
 		});
 	}
 
@@ -198,7 +198,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(REPORT_URI, (String) null);
+			cspListener.blocking().addDirective(REPORT_URI, (String) null);
 		});
 	}
 
@@ -207,7 +207,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(REPORT_URI, "");
+			cspListener.blocking().addDirective(REPORT_URI, "");
 		});
 	}
 
@@ -216,7 +216,7 @@ public class CSPSettingRequestCycleListenerTest
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
 		Assertions.assertThrows(IllegalArgumentException.class, () -> {
-			cspListener.addBlockingDirective(REPORT_URI, "abc?^()-_\'xyz");
+			cspListener.blocking().addDirective(REPORT_URI, "abc?^()-_\'xyz");
 		});
 	}
 
@@ -240,9 +240,9 @@ public class CSPSettingRequestCycleListenerTest
 			{
 				final CSPDirectiveSrcValue cspDirectiveValue =
 					CSPDirectiveSrcValue.values()[i % cspDirectiveSrcValueCount];
-				cspListener.addBlockingDirective(cspDirective, cspDirectiveValue);
+				cspListener.blocking().addDirective(cspDirective, cspDirectiveValue);
 
-				cspListener.addReportingDirective(cspDirective, cspDirectiveValue);
+				cspListener.reporting().addDirective(cspDirective, cspDirectiveValue);
 			}
 		}
 
@@ -258,8 +258,8 @@ public class CSPSettingRequestCycleListenerTest
 	public void testCSPReportUriDirectiveSetCorrectly()
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
-		cspListener.addBlockingDirective(REPORT_URI, "http://report.example.com");
-		cspListener.addReportingDirective(REPORT_URI, "/example-report-uri");
+		cspListener.blocking().addDirective(REPORT_URI, "http://report.example.com");
+		cspListener.reporting().addDirective(REPORT_URI, "/example-report-uri");
 
 		StringBuffer headerErrors = checkHeaders(cspListener);
 
@@ -280,8 +280,8 @@ public class CSPSettingRequestCycleListenerTest
 			if (cspDirectiveValue.equals(CSPDirectiveSandboxValue.EMPTY))
 				continue;
 			
-			cspListener.addBlockingDirective(SANDBOX, cspDirectiveValue);
-			cspListener.addReportingDirective(SANDBOX, cspDirectiveValue);
+			cspListener.blocking().addDirective(SANDBOX, cspDirectiveValue);
+			cspListener.reporting().addDirective(SANDBOX, cspDirectiveValue);
 		}
 
 		StringBuffer headerErrors = checkHeaders(cspListener);
@@ -301,8 +301,8 @@ public class CSPSettingRequestCycleListenerTest
 	public void testChildSrcDirectiveAlsoSetsFrameSrcDirective()
 	{
 		CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener();
-		cspListener.addBlockingDirective(CHILD_SRC, SELF);
-		cspListener.addReportingDirective(CHILD_SRC, SELF);
+		cspListener.blocking().addDirective(CHILD_SRC, SELF);
+		cspListener.reporting().addDirective(CHILD_SRC, SELF);
 		StringBuffer headerErrors = checkHeaders(cspListener);
 
 		if (headerErrors.length() > 0)


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

Posted by pa...@apache.org.
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 1125c3d5b6a35ea4ef6f7886c6d0676baf3c1e88
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;
+	}
+
+}


[wicket] 03/03: WICKET-6727: fix license headers

Posted by pa...@apache.org.
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 ed5c3018b01780f589a815cf8c629624f2060591
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Thu Jan 16 09:27:23 2020 +0100

    WICKET-6727: fix license headers
---
 .../wicket/csp/CSPSettingRequestCycleListener.java | 16 ++++++++++++++++
 .../csp/CspNonceHeaderResponseDecorator.java       | 22 ++++++++++++++--------
 .../csp/CSPSettingRequestCycleListenerTest.java    | 16 ++++++++++++++++
 3 files changed, 46 insertions(+), 8 deletions(-)

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
index 47c6cdd..d688990 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java
@@ -1,3 +1,19 @@
+/*
+ * 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 java.net.URI;
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
index 5aabcda..da87059 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CspNonceHeaderResponseDecorator.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CspNonceHeaderResponseDecorator.java
@@ -1,12 +1,18 @@
 /*
- * 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.
+ * 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;
 
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
index f347f8a..14ba223 100644
--- a/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java
@@ -1,3 +1,19 @@
+/*
+ * 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 static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.CHILD_SRC;