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/24 10:50:31 UTC

[wicket] branch csp-configurable updated: WICKET-6727: easy reporting and use nonce per page

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


The following commit(s) were added to refs/heads/csp-configurable by this push:
     new cc2da2e  WICKET-6727: easy reporting and use nonce per page
cc2da2e is described below

commit cc2da2e0c427c263482facfc10ec05957955afc7
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Fri Jan 24 11:47:44 2020 +0100

    WICKET-6727: easy reporting and use nonce per page
---
 .../java/org/apache/wicket/csp/CSPDirective.java   |   8 +-
 .../wicket/csp/CSPDirectiveSandboxValue.java       |   4 +-
 .../apache/wicket/csp/CSPDirectiveSrcValue.java    |   9 +-
 .../apache/wicket/csp/CSPHeaderConfiguration.java  |  91 +++++++++++++++--
 ...r.java => CSPNonceHeaderResponseDecorator.java} |   7 +-
 .../java/org/apache/wicket/csp/CSPRenderable.java  |   8 +-
 .../wicket/csp/ContentSecurityPolicyEnforcer.java  |  55 ++++++++++-
 .../{FixedCSPDirective.java => FixedCSPValue.java} |  12 ++-
 ...dCSPDirective.java => RelativeURICSPValue.java} |  52 ++++------
 .../wicket/csp/ReportCSPViolationMapper.java       | 110 +++++++++++++++++++++
 .../wicket/protocol/http/WebApplication.java       |  17 ++--
 .../head/filter/FilteringHeaderResponseTest.java   |   7 +-
 12 files changed, 308 insertions(+), 72 deletions(-)

diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirective.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirective.java
index 6507a1c..a73ec66 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirective.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirective.java
@@ -98,14 +98,18 @@ public enum CSPDirective
 					"A report-uri directive can only contain one URI, it already contains "
 						+ existingDirectiveValues);
 			}
-			if (!(value instanceof FixedCSPDirective))
+			if (value instanceof RelativeURICSPValue)
+			{
+				return;
+			}
+			if (!(value instanceof FixedCSPValue))
 			{
 				throw new IllegalArgumentException(
 					"A report-uri directive can only contain an URI, not " + value);
 			}
 			try
 			{
-				new URI(value.render(null, null));
+				new URI(value.toString());
 			}
 			catch (URISyntaxException urise)
 			{
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSandboxValue.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSandboxValue.java
index 8236ee6..d42d72b 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSandboxValue.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSandboxValue.java
@@ -16,6 +16,7 @@
  */
 package org.apache.wicket.csp;
 
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.cycle.RequestCycle;
 
 /**
@@ -42,7 +43,8 @@ public enum CSPDirectiveSandboxValue implements CSPRenderable
 	}
 
 	@Override
-	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle)
+	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle,
+			IRequestHandler currentHandler)
 	{
 		return value;
 	}
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSrcValue.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSrcValue.java
index 0bb4a4e..e897849 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSrcValue.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSrcValue.java
@@ -16,6 +16,7 @@
  */
 package org.apache.wicket.csp;
 
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.cycle.RequestCycle;
 
 /**
@@ -32,9 +33,10 @@ public enum CSPDirectiveSrcValue implements CSPRenderable
 	NONCE("'nonce-%1$s'")
 	{
 		@Override
-		public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle)
+		public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle,
+				IRequestHandler currentHandler)
 		{
-			return String.format(getValue(), listener.getNonce(cycle));
+			return String.format(getValue(), listener.getNonce(cycle, currentHandler));
 		}
 	};
 
@@ -46,7 +48,8 @@ public enum CSPDirectiveSrcValue implements CSPRenderable
 	}
 
 	@Override
-	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle)
+	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle,
+			IRequestHandler currentHandler)
 	{
 		return value;
 	}
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderConfiguration.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderConfiguration.java
index 76c7a6a..836e23b 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderConfiguration.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderConfiguration.java
@@ -22,6 +22,7 @@ import static org.apache.wicket.csp.CSPDirective.DEFAULT_SRC;
 import static org.apache.wicket.csp.CSPDirective.FONT_SRC;
 import static org.apache.wicket.csp.CSPDirective.IMG_SRC;
 import static org.apache.wicket.csp.CSPDirective.MANIFEST_SRC;
+import static org.apache.wicket.csp.CSPDirective.REPORT_URI;
 import static org.apache.wicket.csp.CSPDirective.SCRIPT_SRC;
 import static org.apache.wicket.csp.CSPDirective.STYLE_SRC;
 import static org.apache.wicket.csp.CSPDirectiveSrcValue.NONCE;
@@ -37,6 +38,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.cycle.RequestCycle;
 
 /**
@@ -52,11 +54,15 @@ import org.apache.wicket.request.cycle.RequestCycle;
  */
 public class CSPHeaderConfiguration
 {
+	public static final String CSP_VIOLATION_REPORTING_URI = "cspviolation";
+	
 	private Map<CSPDirective, List<CSPRenderable>> directives = new EnumMap<>(CSPDirective.class);
 
 	private boolean addLegacyHeaders = false;
 	
 	private boolean nonceEnabled = false;
+	
+	private String reportUriMountPath = null;
 
 	public CSPHeaderConfiguration()
 	{
@@ -126,6 +132,54 @@ public class CSPHeaderConfiguration
 	}
 
 	/**
+	 * Configures the CSP to report violations back at the application.
+	 * 
+	 * WARNING: CSP reporting can generate a lot of traffic. A single page load can trigger multiple
+	 * violations and flood your logs or even DDoS your server. In addition, it is an open endpoint
+	 * for your application and can be used by an attacker to flood your application logs. Do not
+	 * enable this feature on a production application unless you take the needed precautions to
+	 * prevent this.
+	 * 
+	 * @return {@code this} for chaining
+	 * @see <a href=
+	 *      "https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp">https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp</a>
+	 */
+	public CSPHeaderConfiguration reportBack()
+	{
+		return reportBackAt(CSP_VIOLATION_REPORTING_URI);
+	}
+
+	/**
+	 * Configures the CSP to report violations at the specified relative URI.
+	 * 
+	 * WARNING: CSP reporting can generate a lot of traffic. A single page load can trigger multiple
+	 * violations and flood your logs or even DDoS your server. In addition, it is an open endpoint
+	 * for your application and can be used by an attacker to flood your application logs. Do not
+	 * enable this feature on a production application unless you take the needed precautions to
+	 * prevent this.
+	 * 
+	 * @param mountPath
+	 *            The path to report the violations at.
+	 * @return {@code this} for chaining
+	 * @see <a href=
+	 *      "https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp">https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp</a>
+	 */
+	public CSPHeaderConfiguration reportBackAt(String mountPath)
+	{
+		return add(REPORT_URI, new RelativeURICSPValue(mountPath));
+	}
+	
+	/**
+	 * Returns the report URI mount path.
+	 * 
+	 * @return the report URI mount path.
+	 */
+	String getReportUriMountPath()
+	{
+		return reportUriMountPath;
+	}
+
+	/**
 	 * True when the {@link CSPDirectiveSrcValue#NONCE} is used in one of the directives.
 	 * 
 	 * @return When any of the directives contains a nonce.
@@ -168,7 +222,7 @@ public class CSPHeaderConfiguration
 	public CSPHeaderConfiguration remove(CSPDirective directive)
 	{
 		directives.remove(directive);
-		return this;
+		return recalculateState();
 	}
 
 	/**
@@ -186,7 +240,7 @@ public class CSPHeaderConfiguration
 		{
 			doAddDirective(directive, value);
 		}
-		return this;
+		return recalculateState();
 	}
 
 	/**
@@ -203,9 +257,9 @@ public class CSPHeaderConfiguration
 	{
 		for (String value : values)
 		{
-			doAddDirective(directive, new FixedCSPDirective(value));
+			doAddDirective(directive, new FixedCSPValue(value));
 		}
-		return this;
+		return recalculateState();
 	}
 
 	/**
@@ -224,7 +278,26 @@ public class CSPHeaderConfiguration
 	public CSPHeaderConfiguration clear()
 	{
 		directives.clear();
-		nonceEnabled = false;
+		return recalculateState();
+	}
+	
+	private CSPHeaderConfiguration recalculateState()
+	{
+		nonceEnabled = directives.values()
+			.stream()
+			.flatMap(List::stream)
+			.anyMatch(value -> value == CSPDirectiveSrcValue.NONCE);
+
+		reportUriMountPath = null;
+		List<CSPRenderable> reportValues = directives.get(CSPDirective.REPORT_URI);
+		if (reportValues != null && !reportValues.isEmpty())
+		{
+			CSPRenderable reportUri = reportValues.get(0);
+			if (reportUri instanceof RelativeURICSPValue)
+			{
+				reportUriMountPath = reportUri.toString();
+			}
+		}
 		return this;
 	}
 
@@ -240,7 +313,6 @@ public class CSPHeaderConfiguration
 		List<CSPRenderable> values = directives.computeIfAbsent(directive, x -> new ArrayList<>());
 		directive.checkValueForDirective(value, values);
 		values.add(value);
-		nonceEnabled |= CSPDirectiveSrcValue.NONCE == value;
 		return this;
 	}
 
@@ -252,16 +324,19 @@ public class CSPHeaderConfiguration
 	 *            The {@link ContentSecurityPolicyEnforcer} that renders the header.
 	 * @param cycle
 	 *            The current {@link RequestCycle}.
+	 * @param currentHandler
+	 *            The handler that is currently being evaluated or executed.
 	 * @return the rendered header.
 	 */
-	public String renderHeaderValue(ContentSecurityPolicyEnforcer listener, RequestCycle cycle)
+	public String renderHeaderValue(ContentSecurityPolicyEnforcer listener, RequestCycle cycle,
+			IRequestHandler currentHandler)
 	{
 		return directives.entrySet()
 			.stream()
 			.map(e -> e.getKey().getValue() + " "
 				+ e.getValue()
 					.stream()
-					.map(r -> r.render(listener, cycle))
+					.map(r -> r.render(listener, cycle, currentHandler))
 					.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
similarity index 90%
rename from wicket-core/src/main/java/org/apache/wicket/csp/CspNonceHeaderResponseDecorator.java
rename to wicket-core/src/main/java/org/apache/wicket/csp/CSPNonceHeaderResponseDecorator.java
index bb0d6ee..8424182 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
@@ -27,11 +27,11 @@ import org.apache.wicket.request.cycle.RequestCycle;
  * Add a <em>Content Security Policy<em> (CSP) nonce to all {@link AbstractCspHeaderItem}s when that
  * is required by the configuration of the CSP.
  */
-public class CspNonceHeaderResponseDecorator extends DecoratingHeaderResponse
+public class CSPNonceHeaderResponseDecorator extends DecoratingHeaderResponse
 {
 	private ContentSecurityPolicyEnforcer listener;
 
-	public CspNonceHeaderResponseDecorator(IHeaderResponse real, ContentSecurityPolicyEnforcer listener)
+	public CSPNonceHeaderResponseDecorator(IHeaderResponse real, ContentSecurityPolicyEnforcer listener)
 	{
 		super(real);
 
@@ -51,7 +51,8 @@ public class CspNonceHeaderResponseDecorator extends DecoratingHeaderResponse
 
 			if (checkitem instanceof AbstractCspHeaderItem)
 			{
-				((AbstractCspHeaderItem) checkitem).setNonce(listener.getNonce(RequestCycle.get()));
+				((AbstractCspHeaderItem) checkitem).setNonce(listener.getNonce(RequestCycle.get(),
+					RequestCycle.get().getActiveRequestHandler()));
 			}
 		}
 
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPRenderable.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPRenderable.java
index 1b67e92..0663490 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPRenderable.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPRenderable.java
@@ -16,6 +16,7 @@
  */
 package org.apache.wicket.csp;
 
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.cycle.RequestCycle;
 
 /**
@@ -25,7 +26,7 @@ import org.apache.wicket.request.cycle.RequestCycle;
  * @author papegaaij
  * @see CSPDirectiveSrcValue
  * @see CSPDirectiveSandboxValue
- * @see FixedCSPDirective
+ * @see FixedCSPValue
  */
 public interface CSPRenderable
 {
@@ -36,9 +37,12 @@ public interface CSPRenderable
 	 *            The {@link ContentSecurityPolicyEnforcer} that renders this value.
 	 * @param cycle
 	 *            The current {@link RequestCycle}.
+	 * @param currentHandler
+	 *            The handler that is currently being evaluated or executed.
 	 * @return The rendered value.
 	 */
-	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle);
+	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle,
+			IRequestHandler currentHandler);
 	
 	/**
 	 * Checks if the {@code CSPRenderable} represents a valid value for a {@code -src} directive. By
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicyEnforcer.java b/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicyEnforcer.java
index ca6c173..54ca9e5 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicyEnforcer.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicyEnforcer.java
@@ -16,14 +16,17 @@
  */
 package org.apache.wicket.csp;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Predicate;
 
 import org.apache.wicket.Application;
 import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.Page;
 import org.apache.wicket.core.request.handler.BufferedResponseRequestHandler;
 import org.apache.wicket.core.request.handler.IPageClassRequestHandler;
+import org.apache.wicket.core.request.handler.IPageRequestHandler;
 import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.IRequestHandlerDelegate;
 import org.apache.wicket.request.cycle.IRequestCycleListener;
@@ -141,7 +144,7 @@ public class ContentSecurityPolicyEnforcer implements IRequestCycleListener
 		configs.entrySet().stream().filter(entry -> entry.getValue().isSet()).forEach(entry -> {
 			CSPHeaderMode mode = entry.getKey();
 			CSPHeaderConfiguration config = entry.getValue();
-			String headerValue = config.renderHeaderValue(this, cycle);
+			String headerValue = config.renderHeaderValue(this, cycle, handler);
 			webResponse.setHeader(mode.getHeader(), headerValue);
 			if (config.isAddLegacyHeaders())
 			{
@@ -160,19 +163,65 @@ public class ContentSecurityPolicyEnforcer implements IRequestCycleListener
 		return configs.values().stream().anyMatch(CSPHeaderConfiguration::isNonceEnabled);
 	}
 
-	public String getNonce(RequestCycle cycle)
+	public String getNonce(RequestCycle cycle, IRequestHandler handler)
 	{
 		String nonce = cycle.getMetaData(NONCE_KEY);
+		Page currentPage = getPage(handler);
 		if (nonce == null)
 		{
-			nonce = getSecuritySettings().getRandomSupplier().getRandomBase64(NONCE_LENGTH);
+			if (currentPage != null)
+			{
+				nonce = currentPage.getMetaData(NONCE_KEY);
+			}
+			if (nonce == null)
+			{
+				nonce = getSecuritySettings().getRandomSupplier().getRandomBase64(NONCE_LENGTH);
+			}
 			cycle.setMetaData(NONCE_KEY, nonce);
 		}
+		if (currentPage != null)
+		{
+			currentPage.setMetaData(NONCE_KEY, nonce);
+		}
 		return nonce;
 	}
 
+	/**
+	 * Resolves a page instance from the request handler iff the page instance is already created
+	 * 
+	 * @param handler
+	 * @return page or {@code null} if none
+	 */
+	public static Page getPage(IRequestHandler handler)
+	{
+		while (handler instanceof IRequestHandlerDelegate)
+		{
+			handler = ((IRequestHandlerDelegate) handler).getDelegateHandler();
+		}
+
+		if (handler instanceof IPageRequestHandler)
+		{
+			IPageRequestHandler pageHandler = (IPageRequestHandler) handler;
+			if (pageHandler.isPageInstanceCreated())
+			{
+				return (Page) pageHandler.getPage();
+			}
+		}
+		return null;
+	}
+
 	private SecuritySettings getSecuritySettings()
 	{
 		return application.getSecuritySettings();
 	}
+	
+	/**
+	 * Returns the CSP configuration per {@link CSPHeaderMode}.
+	 * 
+	 * @return the CSP configuration per {@link CSPHeaderMode}.
+	 */
+	public Map<CSPHeaderMode, CSPHeaderConfiguration> getConfiguration()
+	{
+		return Collections.unmodifiableMap(configs);
+	}
 }
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java b/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPValue.java
similarity index 86%
copy from wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java
copy to wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPValue.java
index b0f56b3..2b73f3d 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPValue.java
@@ -19,25 +19,26 @@ package org.apache.wicket.csp;
 import java.net.URI;
 import java.net.URISyntaxException;
 
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.cycle.RequestCycle;
 import org.apache.wicket.util.string.Strings;
 
 /**
- * A simple CSP directive that renders the string specified.
+ * A simple CSP value that renders the string specified.
  * 
  * @author papegaaij
  */
-public class FixedCSPDirective implements CSPRenderable
+public class FixedCSPValue implements CSPRenderable
 {
 	private String value;
 
 	/**
-	 * Creates a new {@code FixedCSPDirective} for the given value.
+	 * Creates a new {@code FixedCSPValue} for the given value.
 	 * 
 	 * @param value
 	 *            the value to render;
 	 */
-	public FixedCSPDirective(String value)
+	public FixedCSPValue(String value)
 	{
 		if (Strings.isEmpty(value))
 		{
@@ -47,7 +48,8 @@ public class FixedCSPDirective implements CSPRenderable
 	}
 
 	@Override
-	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle)
+	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle,
+			IRequestHandler currentHandler)
 	{
 		return value;
 	}
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java b/wicket-core/src/main/java/org/apache/wicket/csp/RelativeURICSPValue.java
similarity index 56%
rename from wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java
rename to wicket-core/src/main/java/org/apache/wicket/csp/RelativeURICSPValue.java
index b0f56b3..a5feb10 100644
--- a/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/RelativeURICSPValue.java
@@ -19,68 +19,54 @@ package org.apache.wicket.csp;
 import java.net.URI;
 import java.net.URISyntaxException;
 
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.cycle.RequestCycle;
-import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.lang.Args;
 
 /**
- * A simple CSP directive that renders the string specified.
+ * A CSP value that renders an URI relative to the context root of the Wicket application.
  * 
  * @author papegaaij
  */
-public class FixedCSPDirective implements CSPRenderable
+public class RelativeURICSPValue implements CSPRenderable
 {
-	private String value;
+	private String relativeUri;
 
 	/**
-	 * Creates a new {@code FixedCSPDirective} for the given value.
+	 * Creates a new {@code RelativeURICSPValue} for the given relative URI.
 	 * 
-	 * @param value
-	 *            the value to render;
+	 * @param relativeUri
+	 *            The part of the URI relative to the context root of the Wicket application.
 	 */
-	public FixedCSPDirective(String value)
+	public RelativeURICSPValue(String relativeUri)
 	{
-		if (Strings.isEmpty(value))
-		{
-			throw new IllegalArgumentException("CSP directive cannot have empty or null values");
-		}
-		this.value = value;
+		Args.notEmpty(relativeUri, "relativeUri");
+		this.relativeUri = relativeUri;
 	}
 
 	@Override
-	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle)
+	public String render(ContentSecurityPolicyEnforcer listener, RequestCycle cycle,
+			IRequestHandler currentHandler)
 	{
-		return value;
+		return cycle.getUrlRenderer().renderContextRelativeUrl(relativeUri);
 	}
-	
+
 	@Override
 	public void checkValidityForSrc()
 	{
-		String strValue = value;
-		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);
+			new URI("https://example.com/" + relativeUri);
 		}
 		catch (URISyntaxException urise)
 		{
-			throw new IllegalArgumentException("Illegal URI for -src directive", urise);
+			throw new IllegalArgumentException("Illegal relative URI", urise);
 		}
 	}
-	
+
 	@Override
 	public String toString()
 	{
-		return value;
+		return relativeUri;
 	}
 }
\ No newline at end of file
diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/ReportCSPViolationMapper.java b/wicket-core/src/main/java/org/apache/wicket/csp/ReportCSPViolationMapper.java
new file mode 100644
index 0000000..6179060
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/ReportCSPViolationMapper.java
@@ -0,0 +1,110 @@
+package org.apache.wicket.csp;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
+import org.apache.wicket.request.IRequestCycle;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.IRequestMapper;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.mapper.AbstractMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A simple {@link IRequestMapper} that logs the content of a CSP violation.
+ * 
+ * @author papegaaij
+ * @see CSPHeaderConfiguration#reportBack()
+ */
+public class ReportCSPViolationMapper extends AbstractMapper
+{
+	private static final int MAX_LOG_SIZE = 4 * 1024;
+
+	private static final Logger log = LoggerFactory.getLogger(ReportCSPViolationMapper.class);
+
+	private ContentSecurityPolicyEnforcer csp;
+
+	public ReportCSPViolationMapper(ContentSecurityPolicyEnforcer csp)
+	{
+		this.csp = csp;
+	}
+
+	@Override
+	public IRequestHandler mapRequest(Request request)
+	{
+		if (requestMatches(request))
+		{
+			return new IRequestHandler()
+			{
+				@Override
+				public void respond(IRequestCycle requestCycle)
+				{
+					try
+					{
+						HttpServletRequest httpRequest =
+							((ServletWebRequest) requestCycle.getRequest()).getContainerRequest();
+						log.error(reportToString(httpRequest));
+					}
+					catch (IOException e)
+					{
+						throw new WicketRuntimeException(e);
+					}
+				}
+
+				private String reportToString(HttpServletRequest httpRequest) throws IOException
+				{
+					try (StringWriter sw = new StringWriter())
+					{
+						char[] buffer = new char[MAX_LOG_SIZE];
+						int n = 0;
+						if (-1 != (n = httpRequest.getReader().read(buffer)))
+						{
+							sw.write(buffer, 0, n);
+						}
+						return sw.toString();
+					}
+				}
+			};
+		}
+		return null;
+	}
+
+	@Override
+	public int getCompatibilityScore(Request request)
+	{
+		return requestMatches(request) ? 1000 : 0;
+	}
+
+	private boolean requestMatches(Request request)
+	{
+		if (request instanceof ServletWebRequest)
+		{
+			if (!((ServletWebRequest) request).getContainerRequest().getMethod().equals("POST"))
+			{
+				return false;
+			}
+			for (CSPHeaderConfiguration curConfig : csp.getConfiguration().values())
+			{
+				String mountPath = curConfig.getReportUriMountPath();
+				if (mountPath != null
+					&& urlStartsWith(request.getUrl(), getMountSegments(mountPath)))
+				{
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	@Override
+	public Url mapHandler(IRequestHandler requestHandler)
+	{
+		return null;
+	}
+}
diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java
index 1d72fff..0c6dc68 100644
--- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java
+++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java
@@ -42,7 +42,8 @@ import org.apache.wicket.core.util.file.WebApplicationPath;
 import org.apache.wicket.core.util.resource.ClassPathResourceFinder;
 import org.apache.wicket.csp.CSPHeaderConfiguration;
 import org.apache.wicket.csp.ContentSecurityPolicyEnforcer;
-import org.apache.wicket.csp.CspNonceHeaderResponseDecorator;
+import org.apache.wicket.csp.ReportCSPViolationMapper;
+import org.apache.wicket.csp.CSPNonceHeaderResponseDecorator;
 import org.apache.wicket.markup.MarkupType;
 import org.apache.wicket.markup.head.CssHeaderItem;
 import org.apache.wicket.markup.head.JavaScriptHeaderItem;
@@ -749,6 +750,13 @@ public abstract class WebApplication extends Application
 
 		getResourceSettings().setFileCleaner(new FileCleaner());
 
+		cspEnforcer = newCspEnforcer();
+		getRequestCycleListeners().add(getCsp());
+		getHeaderResponseDecorators()
+			.add(response -> new CSPNonceHeaderResponseDecorator(response, getCsp()));
+		mount(new ReportCSPViolationMapper(getCsp()));
+		getCsp().blocking().unsafeInline();
+		
 		if (getConfigurationType() == RuntimeConfigurationType.DEVELOPMENT)
 		{
 			// Add optional sourceFolder for resources.
@@ -757,6 +765,7 @@ public abstract class WebApplication extends Application
 			{
 				getResourceSettings().getResourceFinders().add(new Path(resourceFolder));
 			}
+			getCsp().blocking().reportBack();
 		}
 		setPageRendererProvider(WebPageRenderer::new);
 		setSessionStoreProvider(HttpSessionStore::new);
@@ -764,12 +773,6 @@ public abstract class WebApplication extends Application
 
 		getAjaxRequestTargetListeners().add(new AjaxEnclosureListener());
 
-		cspEnforcer = newCspEnforcer();
-		getRequestCycleListeners().add(getCsp());
-		getHeaderResponseDecorators()
-			.add(response -> new CspNonceHeaderResponseDecorator(response, getCsp()));
-		getCsp().blocking().unsafeInline();
-		
 		// Configure the app.
 		configure();
 	}
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java
index 2d5c467..0463fab 100644
--- a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java
@@ -20,15 +20,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.util.Collections;
 
-import org.apache.wicket.csp.CSPDirective;
 import org.apache.wicket.csp.ContentSecurityPolicyEnforcer;
-import org.apache.wicket.markup.head.IHeaderResponse;
-import org.apache.wicket.markup.head.ResourceAggregator;
 import org.apache.wicket.markup.head.StringHeaderItem;
 import org.apache.wicket.markup.head.internal.HeaderResponse;
-import org.apache.wicket.markup.html.IHeaderResponseDecorator;
 import org.apache.wicket.mock.MockApplication;
 import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.Response;
 import org.apache.wicket.request.cycle.RequestCycle;
 import org.apache.wicket.response.StringResponse;
@@ -53,7 +50,7 @@ class FilteringHeaderResponseTest extends WicketTestCase
 				return new ContentSecurityPolicyEnforcer(this)
 				{
 					@Override
-					public String getNonce(RequestCycle cycle)
+					public String getNonce(RequestCycle cycle, IRequestHandler currentHandler)
 					{
 						return "NONCE";
 					}