You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by sv...@apache.org on 2019/08/05 20:10:42 UTC

[wicket] branch master updated: WICKET-6682 add CSP nonce support

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4027b54  WICKET-6682 add CSP nonce support
4027b54 is described below

commit 4027b5456031dae79af7ca0a14a1fe59719653ad
Author: Andrew Kondratev <an...@gmail.com>
AuthorDate: Tue Jul 23 17:43:42 2019 +0200

    WICKET-6682 add CSP nonce support
    
    reverts WICKET-4777 since all markup attributes have to be encoded
    
    this closes #376
---
 .../apache/wicket/core/util/string/CssUtils.java   | 100 +++++++++++-----
 .../wicket/core/util/string/JavaScriptUtils.java   | 129 +++++++++++++++------
 .../wicket/markup/head/AbstractCspHeaderItem.java  | 102 ++++++++--------
 .../wicket/markup/head/CssContentHeaderItem.java   |   6 +-
 .../apache/wicket/markup/head/CssHeaderItem.java   |  15 ++-
 .../markup/head/JavaScriptContentHeaderItem.java   |   9 +-
 .../wicket/markup/head/JavaScriptHeaderItem.java   |  22 ++--
 .../markup/head/JavaScriptReferenceHeaderItem.java |   2 -
 .../wicket/markup/head/MetaDataHeaderItem.java     |  35 ++++++
 .../wicket/markup/head/OnDomReadyHeaderItem.java   |  10 +-
 .../wicket/markup/head/OnEventHeaderItem.java      |   8 +-
 .../wicket/markup/head/OnLoadHeaderItem.java       |  10 +-
 .../markup/head/filter/CspNonceHeaderResponse.java |  93 +++++++++++++++
 .../wicket/markup/html/pages/BrowserInfoPage.java  |   2 +-
 .../org/apache/wicket/markup/parser/XmlTag.java    |  64 +++-------
 .../wicket/core/util/string/CssUtilsTest.java      |  17 ++-
 .../core/util/string/JavaScriptUtilsTest.java      |  47 ++++----
 .../wicket/markup/head/filter/CspNoncePage.html    |   4 +
 .../wicket/markup/head/filter/CspNoncePage.java    |  96 ++++++++-------
 .../markup/head/filter/CspNoncePageExpected.html   |  25 ++++
 .../head/filter/FilteringHeaderResponseTest.java   |  11 ++
 .../wicket/page/XmlPartialPageUpdateTest.java      |   2 +-
 .../apache/wicket/examples/csp/CspApplication.java |  72 ++++++++++++
 .../apache/wicket/examples/csp/NonceDemoPage.html  |  34 ++++++
 .../apache/wicket/examples/csp/NonceDemoPage.java  |  81 +++++++++++++
 .../wicket/examples/csp/NonceDemoPage.properties   |  21 ++++
 .../apache/wicket/examples/homepage/HomePage.html  |   1 +
 wicket-examples/src/main/webapp/WEB-INF/web.xml    |  16 +++
 .../org/apache/wicket/util/value/AttributeMap.java |  87 +++++++++++++-
 .../org/apache/wicket/util/value/ValueMap.java     |   3 +-
 .../apache/wicket/util/value/AttributeMapTest.java |  69 +++++++++++
 .../org/apache/wicket/util/value/ValueMapTest.java |   3 +
 32 files changed, 937 insertions(+), 259 deletions(-)

diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java b/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java
index b028b43..521decf 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java
@@ -19,6 +19,7 @@ package org.apache.wicket.core.util.string;
 import org.apache.wicket.request.Response;
 import org.apache.wicket.util.lang.Classes;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * Utility methods for CSS.
@@ -27,6 +28,7 @@ import org.apache.wicket.util.string.Strings;
  */
 public final class CssUtils
 {
+	// FIXME type text/css can be omitted for the style tag in supported browsers
 	/** start of CSS inline open tag */
 	public final static String INLINE_OPEN_TAG_START = "<style type=\"text/css\"";
 
@@ -36,6 +38,13 @@ public final class CssUtils
 	/** CSS inline close tag */
 	public final static String INLINE_CLOSE_TAG = "</style>\n";
 
+	public static final String ATTR_ID = "id";
+	public static final String ATTR_TYPE = "type";
+	public static final String ATTR_LINK_HREF = "href";
+	public static final String ATTR_LINK_MEDIA = "media";
+	public static final String ATTR_LINK_REL = "rel";
+	public static final String ATTR_CSP_NONCE = "nonce";
+
 	/**
 	 * Hidden constructor.
 	 */
@@ -52,7 +61,9 @@ public final class CssUtils
 	 *            The text to added in between the style tags
 	 * @param id
 	 *            Unique identifier of element
+	 * @deprecated please use {@link #writeInlineStyle(Response, CharSequence, AttributeMap)} instead
 	 */
+	@Deprecated
 	public static void writeCss(final Response response, final CharSequence text, String id)
 	{
 		writeOpenTag(response, id);
@@ -61,17 +72,49 @@ public final class CssUtils
 	}
 
 	/**
+	 * Write the simple text to the response object surrounded by a style tag.
+	 * In most cases the text simply an inline CSS.
+	 *
+	 * @param response
+	 * 		The HTTP: response
+	 * @param text
+	 * 		The text to added in between the style tags
+	 * @param attributes
+	 * 		Tag attributes map
+	 * @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style">Style HTML Element</a>
+	 */
+	public static void writeInlineStyle(final Response response, final CharSequence text, AttributeMap attributes)
+	{
+		writeOpenTag(response, attributes);
+		response.write(text);
+		writeCloseTag(response);
+	}
+
+	/**
+	 * Write open style tag for the inline CSS
 	 *
 	 * @param response
 	 * @param id
 	 */
 	public static void writeOpenTag(final Response response, String id)
 	{
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(ATTR_ID, id);
+		writeOpenTag(response, attributes);
+	}
+
+	/**
+	 * Write open style tag for the inline CSS
+	 *
+	 * @param response
+	 * 		the response to write to
+	 * @param attributes
+	 * 		Tag attributes map
+	 */
+	public static void writeOpenTag(final Response response, AttributeMap attributes)
+	{
 		response.write(INLINE_OPEN_TAG_START);
-		if (id != null)
-		{
-			response.write(" id=\"" + id + "\"");
-		}
+		response.write(attributes.toCharSequence());
 		response.write(">\n");
 	}
 
@@ -95,7 +138,9 @@ public final class CssUtils
 	 *            the CSS media
 	 * @param markupId
 	 *            the markupId
+	 * @deprecated please use {@link #writeLink(Response, AttributeMap)} instead
 	 */
+	@Deprecated
 	public static void writeLinkUrl(final Response response, final CharSequence url,
 		final CharSequence media, final String markupId)
 	{
@@ -115,31 +160,34 @@ public final class CssUtils
 	 *            the markupId
 	 * @param rel
 	 *            the rel attribute
+	 * @deprecated please use {@link #writeLink(Response, AttributeMap)} instead
 	 */
+	@Deprecated
 	public static void writeLinkUrl(final Response response, final CharSequence url,
 		final CharSequence media, final String markupId, final String rel)
 	{
-		response.write("<link rel=\"stylesheet\" type=\"text/css\" href=\"");
-		response.write(Strings.escapeMarkup(url));
-		response.write("\"");
-		if (Strings.isEmpty(media) == false)
-		{
-			response.write(" media=\"");
-			response.write(Strings.escapeMarkup(media));
-			response.write("\"");
-		}
-		if (Strings.isEmpty(markupId) == false)
-		{
-			response.write(" id=\"");
-			response.write(Strings.escapeMarkup(markupId));
-			response.write("\"");
-		}
-		if (Strings.isEmpty(rel) == false)
-		{
-			response.write(" rel=\"");
-			response.write(Strings.escapeMarkup(rel));
-			response.write("\"");
-		}
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(ATTR_LINK_REL, Strings.isEmpty(rel) ? "stylesheet" : rel);
+		attributes.putAttribute(ATTR_TYPE, "text/css");
+		attributes.putAttribute(ATTR_LINK_HREF, url);
+		attributes.putAttribute(ATTR_LINK_MEDIA, media.toString());
+		attributes.putAttribute(ATTR_ID, markupId);
+		writeLink(response, attributes);
+	}
+
+	/**
+	 * Writes a reference to a css file in the response object
+	 *
+	 * @param response
+	 * 		the response to write to
+	 * @param attributes
+	 * 		Attributes map
+	 * @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link">Link HTML Element</a>
+	 */
+	public static void writeLink(final Response response, AttributeMap attributes)
+	{
+		response.write("<link");
+		response.write(attributes.toCharSequence());
 		response.write(" />");
 	}
 
@@ -156,4 +204,4 @@ public final class CssUtils
 	{
 		return Classes.simpleName(scope) + ".CSS." + facet;
 	}
-}
+}
\ No newline at end of file
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java b/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java
index 8e4d042..08b6a22 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java
@@ -18,7 +18,7 @@ package org.apache.wicket.core.util.string;
 
 import org.apache.wicket.request.Response;
 import org.apache.wicket.util.string.Strings;
-
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * Provide some helpers to write javascript related tags to the response object.
@@ -46,6 +46,13 @@ public class JavaScriptUtils
 	/** Script close tag including content suffix */
 	public final static String SCRIPT_CLOSE_TAG = SCRIPT_CONTENT_SUFFIX + "</script>\n";
 
+	public static final String ATTR_ID = "id";
+	public static final String ATTR_TYPE = "type";
+	public static final String ATTR_SCRIPT_SRC = "src";
+	public static final String ATTR_SCRIPT_DEFER = "defer";
+	public static final String ATTR_SCRIPT_ASYNC = "async";
+	public static final String ATTR_CSP_NONCE = "nonce";
+
 	/** The response object */
 	private final Response response;
 
@@ -104,7 +111,9 @@ public class JavaScriptUtils
 	 *            The javascript file URL
 	 * @param id
 	 *            Unique identifier of element
+	 * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
 	 */
+	@Deprecated
 	public static void writeJavaScriptUrl(final Response response, final CharSequence url,
 		final String id)
 	{
@@ -125,7 +134,9 @@ public class JavaScriptUtils
 	 *            the page has been loaded.
 	 * @param charset
 	 *            a non null value specifies the charset attribute of the script tag
+	 * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
 	 */
+	@Deprecated
 	public static void writeJavaScriptUrl(final Response response, final CharSequence url,
 	                                      final String id, boolean defer, String charset)
 	{
@@ -148,32 +159,38 @@ public class JavaScriptUtils
 	 *            a non null value specifies the charset attribute of the script tag
 	 * @param async
 	 *            specifies that the script can be loaded asynchronously by the browser
+	 * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
 	 */
+	@Deprecated
 	public static void writeJavaScriptUrl(final Response response, final CharSequence url,
 		final String id, boolean defer, String charset, boolean async)
 	{
-		response.write("<script type=\"text/javascript\" ");
-		if (id != null)
-		{
-			response.write("id=\"" + Strings.escapeMarkup(id) + "\" ");
-		}
-		if (defer)
-		{
-			response.write("defer=\"defer\" ");
-		}
-
-		if (async)
-		{
-			response.write("async=\"async\" ");
-		}
+		AttributeMap attributes = new AttributeMap();
+		// XXX JS mimetype can be omitted (also see below)
+		attributes.putAttribute(ATTR_TYPE, "text/javascript");
+		attributes.putAttribute(ATTR_SCRIPT_SRC, url);
+		attributes.putAttribute(ATTR_ID, id);
+		attributes.putAttribute(ATTR_SCRIPT_DEFER, defer);
+		attributes.putAttribute(ATTR_SCRIPT_ASYNC, async);
+		// FIXME charset attr is deprecated
+		// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#Deprecated_attributes
+		attributes.putAttribute("charset", charset);
+		writeScript(response, attributes);
+	}
 
-		if (charset != null)
-		{
-			response.write("charset=\"" + Strings.escapeMarkup(charset) + "\" ");
-		}
-		response.write("src=\"");
-		response.write(url);
-		response.write("\"></script>");
+	/**
+	 * Write a reference to a javascript file to the response object
+	 *
+	 * @param response
+	 *            The HTTP response
+	 * @param attributes
+	 *            Extra tag attributes
+	 */
+	public static void writeScript(final Response response, AttributeMap attributes)
+	{
+		response.write("<script");
+		response.write(attributes.toCharSequence());
+		response.write("></script>");
 		response.write("\n");
 	}
 
@@ -184,7 +201,9 @@ public class JavaScriptUtils
 	 *            The HTTP response
 	 * @param url
 	 *            The javascript file URL
+	 * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
 	 */
+	@Deprecated
 	public static void writeJavaScriptUrl(final Response response, final CharSequence url)
 	{
 		writeJavaScriptUrl(response, url, null);
@@ -199,6 +218,7 @@ public class JavaScriptUtils
 	 *            The text to added in between the script tags
 	 * @param id
 	 *            Unique identifier of element
+	 * @deprecated please use {@link #writeInlineScript(Response, CharSequence, AttributeMap)} instead
 	 */
 	public static void writeJavaScript(final Response response, final CharSequence text, String id)
 	{
@@ -209,6 +229,23 @@ public class JavaScriptUtils
 
 	/**
 	 * Write the simple text to the response object surrounded by a script tag.
+	 *
+	 * @param response
+	 * 		The HTTP: response
+	 * @param text
+	 * 		The text to added in between the script tags
+	 * @param attributes
+	 * 		Extra tag attributes
+	 */
+	public static void writeInlineScript(final Response response, final CharSequence text, AttributeMap attributes)
+	{
+		writeOpenTag(response, attributes);
+		response.write(Strings.replaceAll(text, "</", "<\\/"));
+		writeCloseTag(response);
+	}
+
+	/**
+	 * Write the simple text to the response object surrounded by a script tag.
 	 * 
 	 * @param response
 	 *            The HTTP: response
@@ -217,21 +254,39 @@ public class JavaScriptUtils
 	 */
 	public static void writeJavaScript(final Response response, final CharSequence text)
 	{
-		writeJavaScript(response, text, null);
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(ATTR_TYPE, "text/javascript");
+		writeInlineScript(response, text, attributes);
 	}
 
 	/**
 	 * 
 	 * @param response
 	 * @param id
+	 * @deprecated please use {@link #writeOpenTag(Response, AttributeMap)}
 	 */
+	@Deprecated
 	public static void writeOpenTag(final Response response, String id)
 	{
-		response.write("<script type=\"text/javascript\" ");
-		if (id != null)
-		{
-			response.write("id=\"" + Strings.escapeMarkup(id) + "\"");
-		}
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(ATTR_TYPE, "text/javascript");
+		attributes.putAttribute(ATTR_ID, id);
+		writeOpenTag(response, attributes);
+	}
+
+	/**
+	 * Write open script tag for inline script.
+	 * Content is prefixed with {@link #SCRIPT_CONTENT_PREFIX}.
+	 *
+	 * @param response
+	 * 		the response to write to
+	 * @param attributes
+	 * 		Tag attributes map
+	 */
+	public static void writeOpenTag(final Response response, AttributeMap attributes)
+	{
+		response.write("<script");
+		response.write(attributes.toCharSequence());
 		response.write(">");
 		response.write(SCRIPT_CONTENT_PREFIX);
 	}
@@ -239,15 +294,21 @@ public class JavaScriptUtils
 	/**
 	 * 
 	 * @param response
+	 * @deprecated please use {@link #writeOpenTag(Response, AttributeMap)}
 	 */
+	@Deprecated
 	public static void writeOpenTag(final Response response)
 	{
-		writeOpenTag(response, null);
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(ATTR_TYPE, "text/javascript");
+		writeOpenTag(response, attributes);
 	}
 
 	/**
-	 * 
+	 * Write close script tag for inline script. The close tag is prefixed with {@link #SCRIPT_CONTENT_SUFFIX}
+	 *
 	 * @param response
+	 * 		the response to write to
 	 */
 	public static void writeCloseTag(final Response response)
 	{
@@ -274,10 +335,12 @@ public class JavaScriptUtils
 	}
 
 	/**
-	 * Write the script close tag to the response. The response output stream remains open.
+	 * Write the inline script close tag to the response. The response output stream remains open.
+	 * Calls {@link #writeCloseTag(Response)} internally.
+	 * The close tag is prefixed with {@link #SCRIPT_CONTENT_SUFFIX}.
 	 */
 	public void close()
 	{
 		writeCloseTag(response);
-	}
-}
+	}	
+}
\ No newline at end of file
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/AbstractCspHeaderItem.java
old mode 100755
new mode 100644
similarity index 61%
copy from wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
copy to wicket-core/src/main/java/org/apache/wicket/markup/head/AbstractCspHeaderItem.java
index 8a6f297..d754c86
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/AbstractCspHeaderItem.java
@@ -1,49 +1,53 @@
-/*
- * 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.util.value;
-
-import java.util.Map;
-
-/**
- * <code>ValueMap</code> for attributes.
- * 
- * @author Eelco Hillenius
- * @since 1.2.6
- */
-public final class AttributeMap extends ValueMap
-{
-	private static final long serialVersionUID = 1L;
-
-	/**
-	 * Constructs an empty <code>AttributeMap</code>.
-	 */
-	public AttributeMap()
-	{
-		super();
-	}
-
-	/**
-	 * Copy constructor.
-	 * 
-	 * @param map
-	 *            a <code>Map</code> to be copied
-	 */
-	public AttributeMap(final Map<String, Object> map)
-	{
-		super(map);
-	}
-}
+/*
+ * 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.markup.head;
+
+/**
+ * Abstract base class for all {@link HeaderItem}s that have a <em>nonce</em> to
+ * allow the browser to check <em>Content Security Policye</em> (CSP) .
+ * 
+ * @author svenmeier
+ */
+public abstract class AbstractCspHeaderItem extends HeaderItem
+{
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * CSP Nonce
+	 */
+	private String nonce;
+
+	/**
+	 * @return CSP nonce
+	 */
+	public String getNonce()
+	{
+		return nonce;
+	}
+
+	/**
+	 * Set the CSP nonce
+	 * 
+	 * @param nonce
+	 * @return {@code this} object, for method chaining
+	 */
+	public AbstractCspHeaderItem setNonce(String nonce)
+	{
+		this.nonce = nonce;
+		return this;
+	}
+}
\ No newline at end of file
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java
index dee0a63..97d14bd 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java
@@ -23,6 +23,7 @@ import java.util.Objects;
 import org.apache.wicket.core.util.string.CssUtils;
 import org.apache.wicket.request.Response;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * {@link HeaderItem} for internal (embedded in the header) css content.
@@ -72,7 +73,10 @@ public class CssContentHeaderItem extends CssHeaderItem
 			response.write("]>");
 		}
 
-		CssUtils.writeCss(response, getCss(), getId());
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(CssUtils.ATTR_ID, getId());
+		attributes.putAttribute(CssUtils.ATTR_CSP_NONCE, getNonce());
+		CssUtils.writeInlineStyle(response, getCss(), attributes);
 
 		if (hasCondition)
 		{
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java
index c269d8f..8ac04a3 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java
@@ -26,6 +26,7 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.ResourceReference;
 import org.apache.wicket.util.lang.Args;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -35,7 +36,7 @@ import org.slf4j.LoggerFactory;
  * 
  * @author papegaaij
  */
-public abstract class CssHeaderItem extends HeaderItem
+public abstract class CssHeaderItem extends AbstractCspHeaderItem
 {
 	private static final long serialVersionUID = 1L;
 
@@ -61,7 +62,7 @@ public abstract class CssHeaderItem extends HeaderItem
 	private final String condition;
 
 	private String markupId;
-	
+
 	protected CssHeaderItem(String condition)
 	{
 		this.condition = condition;
@@ -355,8 +356,14 @@ public abstract class CssHeaderItem extends HeaderItem
 			response.write(condition);
 			response.write("]>");
 		}
-
-		CssUtils.writeLinkUrl(response, url, media, getId(), rel);
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(CssUtils.ATTR_LINK_REL, rel == null ? "stylesheet" : rel);
+		attributes.putAttribute(CssUtils.ATTR_TYPE, "text/css");
+		attributes.putAttribute(CssUtils.ATTR_LINK_HREF, url);
+		attributes.putAttribute(CssUtils.ATTR_ID, getId());
+		attributes.putAttribute(CssUtils.ATTR_LINK_MEDIA, media);
+		attributes.putAttribute(CssUtils.ATTR_CSP_NONCE, getNonce());
+		CssUtils.writeLink(response, attributes);
 
 		if (hasCondition)
 		{
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java
index 2b3f8d5..fccadee 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java
@@ -20,9 +20,10 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Objects;
 
-import org.apache.wicket.request.Response;
 import org.apache.wicket.core.util.string.JavaScriptUtils;
+import org.apache.wicket.request.Response;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * {@link HeaderItem} for internal (embedded in the header) javascript content.
@@ -69,7 +70,11 @@ public class JavaScriptContentHeaderItem extends JavaScriptHeaderItem
 			response.write(getCondition());
 			response.write("]>");
 		}
-		JavaScriptUtils.writeJavaScript(response, getJavaScript(), getId());
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+		attributes.putAttribute(JavaScriptUtils.ATTR_ID, getId());
+		attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+		JavaScriptUtils.writeInlineScript(response, getJavaScript(), attributes);
 
 		if (hasCondition)
 		{
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java
index 0b43443..e72f834 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java
@@ -18,14 +18,13 @@ package org.apache.wicket.markup.head;
 
 import java.util.Objects;
 
-import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
 import org.apache.wicket.core.util.string.JavaScriptUtils;
 import org.apache.wicket.request.Response;
-import org.apache.wicket.request.cycle.RequestCycle;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.ResourceReference;
 import org.apache.wicket.util.lang.Args;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * Base class for all {@link HeaderItem}s that represent javascripts. This class mainly contains
@@ -33,8 +32,10 @@ import org.apache.wicket.util.string.Strings;
  * 
  * @author papegaaij
  */
-public abstract class JavaScriptHeaderItem extends HeaderItem
+public abstract class JavaScriptHeaderItem extends AbstractCspHeaderItem
 {
+	private static final long serialVersionUID = 1L;
+
 	/**
 	 * The condition to use for Internet Explorer conditional comments. E.g. "IE 7".
 	 * {@code null} or empty string for no condition.
@@ -362,11 +363,16 @@ public abstract class JavaScriptHeaderItem extends HeaderItem
 			response.write("]>");
 		}
 
-		boolean isAjax = RequestCycle.get().find(IPartialPageRequestHandler.class).isPresent();
-		// the url needs to be escaped when Ajax, because it will break the Ajax Response XML (WICKET-4777)
-		CharSequence escapedUrl = isAjax ? Strings.escapeMarkup(url): url;
-
-		JavaScriptUtils.writeJavaScriptUrl(response, escapedUrl, id, defer, charset, async);
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+		attributes.putAttribute(JavaScriptUtils.ATTR_ID, id);
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_DEFER, defer);
+		// XXX this attribute is not necessary for modern browsers
+		attributes.putAttribute("charset", charset);
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_ASYNC, async);
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_SRC, url);
+		attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+		JavaScriptUtils.writeScript(response, attributes);
 
 		if (hasCondition)
 		{
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java
index ca52cfb..c5209cc 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java
@@ -20,7 +20,6 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import org.apache.wicket.core.util.lang.WicketObjects;
 import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.Response;
 import org.apache.wicket.request.cycle.RequestCycle;
@@ -29,7 +28,6 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.ResourceReference;
 import org.apache.wicket.resource.bundles.IResourceBundle;
 import org.apache.wicket.util.lang.Args;
-import org.apache.wicket.util.lang.Objects;
 import org.apache.wicket.util.string.Strings;
 
 /**
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java
index 77fce4b..38efb5b 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java
@@ -162,6 +162,41 @@ public class MetaDataHeaderItem extends HeaderItem
 	/**
 	 * Factory method to create &lt;meta&gt; tag.
 	 * 
+	 * @param httpEquiv
+	 * 		the 'httpEquiv' attribute of the tag
+	 * @param content
+	 * 		the 'content' attribute of the tag
+	 * @return
+	 * 		A new {@link MetaDataHeaderItem}
+	 */
+	public static MetaDataHeaderItem forHttpEquiv(String httpEquiv, String content)
+	{
+		return forHttpEquiv(Model.of(httpEquiv), Model.of(content));
+	}
+
+	/**
+	 * Factory method to create &lt;meta&gt; tag.
+	 * 
+	 * @param httpEquiv
+	 * 		the 'httpEquiv' attribute of the tag
+	 * @param content
+	 * 		the 'content' attribute of the tag
+	 * @return
+	 * 		A new {@link MetaDataHeaderItem}
+	 */
+	public static MetaDataHeaderItem forHttpEquiv(IModel<String> httpEquiv, IModel<String> content)
+	{
+		MetaDataHeaderItem headerItem = new MetaDataHeaderItem(META_TAG);
+
+		headerItem.addTagAttribute("http-equiv", httpEquiv);
+		headerItem.addTagAttribute("content", content);
+
+		return headerItem;
+	}
+
+	/**
+	 * Factory method to create &lt;meta&gt; tag.
+	 * 
 	 * @param name
 	 * 		the 'name' attribute of the tag
 	 * @param content
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java
index cc51588..d3a829d 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java
@@ -26,6 +26,7 @@ import org.apache.wicket.request.Response;
 import org.apache.wicket.request.resource.ResourceReference;
 import org.apache.wicket.settings.JavaScriptLibrarySettings;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * {@link HeaderItem} for scripts that need to be executed directly after the DOM has been built,
@@ -33,7 +34,7 @@ import org.apache.wicket.util.string.Strings;
  *
  * @author papegaaij
  */
-public class OnDomReadyHeaderItem extends HeaderItem
+public class OnDomReadyHeaderItem extends AbstractCspHeaderItem
 {
 	private static final long serialVersionUID = 1L;
 
@@ -86,8 +87,11 @@ public class OnDomReadyHeaderItem extends HeaderItem
 		CharSequence js = getJavaScript();
 		if (Strings.isEmpty(js) == false)
 		{
-			JavaScriptUtils.writeJavaScript(response, "Wicket.Event.add(window, \"domready\", " +
-				"function(event) { " + js + ";});");
+			AttributeMap attributes = new AttributeMap();
+			attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+			attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+			JavaScriptUtils.writeInlineScript(response, "Wicket.Event.add(window, \"domready\", " +
+					"function(event) { " + js + ";});", attributes);
 		}
 	}
 
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java
index fd1bf60..f3b7baf 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java
@@ -28,13 +28,14 @@ import org.apache.wicket.request.resource.ResourceReference;
 import org.apache.wicket.settings.JavaScriptLibrarySettings;
 import org.apache.wicket.util.lang.Args;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * {@link HeaderItem} for event triggered scripts.
  *
  * @author papegaaij
  */
-public class OnEventHeaderItem extends HeaderItem
+public class OnEventHeaderItem extends AbstractCspHeaderItem
 {
 	private static final long serialVersionUID = 1L;
 
@@ -118,7 +119,10 @@ public class OnEventHeaderItem extends HeaderItem
 	{
 		if (Strings.isEmpty(getJavaScript()) == false)
 		{
-			JavaScriptUtils.writeJavaScript(response, getCompleteJavaScript());
+			AttributeMap attributes = new AttributeMap();
+			attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+			attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+			JavaScriptUtils.writeInlineScript(response, getCompleteJavaScript(), attributes);
 		}
 	}
 
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java
index 3cd2cde..1ceb4fc 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java
@@ -26,13 +26,14 @@ import org.apache.wicket.request.Response;
 import org.apache.wicket.request.resource.ResourceReference;
 import org.apache.wicket.settings.JavaScriptLibrarySettings;
 import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 
 /**
  * {@link HeaderItem} for scripts that need to be executed after the entire page is loaded.
  *
  * @author papegaaij
  */
-public class OnLoadHeaderItem extends HeaderItem
+public class OnLoadHeaderItem extends AbstractCspHeaderItem
 {
 	private static final long serialVersionUID = 1L;
 
@@ -86,8 +87,11 @@ public class OnLoadHeaderItem extends HeaderItem
 		CharSequence js = getJavaScript();
 		if (Strings.isEmpty(js) == false)
 		{
-			JavaScriptUtils.writeJavaScript(response, "Wicket.Event.add(window, \"load\", " +
-				"function(event) { " + js + ";});");
+			AttributeMap attributes = new AttributeMap();
+			attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+			attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+			JavaScriptUtils.writeInlineScript(response, "Wicket.Event.add(window, \"load\", " +
+					"function(event) { " + js + ";});", attributes);
 		}
 	}
 
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/filter/CspNonceHeaderResponse.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/filter/CspNonceHeaderResponse.java
new file mode 100644
index 0000000..7a911ca
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/filter/CspNonceHeaderResponse.java
@@ -0,0 +1,93 @@
+/*
+ * 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.markup.head.filter;
+
+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.MetaDataHeaderItem;
+import org.apache.wicket.markup.head.ResourceAggregator;
+import org.apache.wicket.markup.html.DecoratingHeaderResponse;
+
+/**
+ * Add a <em>Content Security Policy<em> (CSP) nonce to all relevant JavaScript and CSS header items.
+ * <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 CspNonceHeaderResponse extends DecoratingHeaderResponse
+{
+	private static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
+
+	/**
+	 * Has the <em>Content-Security-Policy</em> header been rendered once.
+	 */
+	private boolean policyRendered = false;
+
+	private String nonce;
+
+	public CspNonceHeaderResponse(IHeaderResponse real, String nonce)
+	{
+		super(real);
+
+		this.nonce = nonce;
+	}
+
+	@Override
+	public void render(HeaderItem item)
+	{
+		while (item instanceof IWrappedHeaderItem)
+		{
+			item = ((IWrappedHeaderItem)item).getWrapped();
+		}
+
+		if (item instanceof AbstractCspHeaderItem)
+		{
+			if (policyRendered == false)
+			{
+				policyRendered = true;
+
+				String policy = getContentSecurityPolicy(nonce);
+
+				super.render(MetaDataHeaderItem.forHttpEquiv(CONTENT_SECURITY_POLICY, policy));
+			}
+
+			((AbstractCspHeaderItem)item).setNonce(nonce);
+		}
+		
+		super.render(item);
+	}
+
+	/**
+	 * Get the <em>Content-Security-Policy</em> (CSP).
+	 * <p>
+	 * There is a variety of CSP configurations, this default implementation uses the nonce for scripts and styles
+	 * and allows <code>unsafe-eval</code>s (needed for Wicket Ajax).
+	 * 
+	 * @param nonce
+	 *            the nonce
+	 * @return content security policy
+	 */
+	protected String getContentSecurityPolicy(String nonce)
+	{
+		return String.format("script-src 'unsafe-eval' 'nonce-%1$s'; style-src 'nonce-%1$s';", nonce);
+	}
+}
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java
index 8d2cdc8..0541fa0 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java
@@ -139,7 +139,7 @@ public class BrowserInfoPage extends WebPage
 		{
 			String content = "0; url=" + getURL();
 
-			response.render(new MetaDataHeaderItem(MetaDataHeaderItem.META_TAG).addTagAttribute("http-equiv", "refresh").addTagAttribute("content", content));
+			response.render(MetaDataHeaderItem.forHttpEquiv("refresh", content));
 		}
 		
 		@Override
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java b/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java
index 530bd29..eb4f276 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java
@@ -16,16 +16,13 @@
  */
 package org.apache.wicket.markup.parser;
 
-import java.util.Iterator;
 import java.util.Map;
 
-import org.apache.wicket.markup.parser.IXmlPullParser.HttpTagType;
 import org.apache.wicket.util.lang.Objects;
 import org.apache.wicket.util.string.AppendingStringBuffer;
 import org.apache.wicket.util.string.StringValue;
-import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
 import org.apache.wicket.util.value.IValueMap;
-import org.apache.wicket.util.value.ValueMap;
 
 
 /**
@@ -41,26 +38,19 @@ public class XmlTag
 	 */
 	public static enum TagType {
 		/** A close tag, like &lt;/TAG&gt;. */
-		CLOSE("CLOSE"),
+		CLOSE,
 
 		/** An open tag, like &lt;TAG componentId = "xyz"&gt;. */
-		OPEN("OPEN"),
+		OPEN,
 
 		/** An open/close tag, like &lt;TAG componentId = "xyz"/&gt;. */
-		OPEN_CLOSE("OPEN_CLOSE");
-
-		private String name;
-
-		TagType(final String name)
-		{
-			this.name = name;
-		}
+		OPEN_CLOSE;
 	}
 
 	TextSegment text;
 
 	/** Attribute map. */
-	private IValueMap attributes;
+	private AttributeMap attributes;
 
 	/** Name of tag, such as "img" or "input". */
 	String name;
@@ -80,8 +70,6 @@ public class XmlTag
 	/** True if this tag is mutable, false otherwise. */
 	private boolean isMutable = true;
 
-	private HttpTagType httpTagType;
-
 	/**
 	 * Construct.
 	 */
@@ -139,15 +127,20 @@ public class XmlTag
 	 */
 	public IValueMap getAttributes()
 	{
+		return attributes();
+	}
+
+	private AttributeMap attributes()
+	{
 		if (attributes == null)
 		{
 			if ((copyOf == this) || (copyOf == null) || (copyOf.attributes == null))
 			{
-				attributes = new ValueMap();
+				attributes = new AttributeMap();
 			}
 			else
 			{
-				attributes = new ValueMap(copyOf.attributes);
+				attributes = new AttributeMap(copyOf.attributes);
 			}
 		}
 		return attributes;
@@ -349,7 +342,7 @@ public class XmlTag
 		dest.copyOf = copyOf;
 		if (attributes != null)
 		{
-			dest.attributes = new ValueMap(attributes);
+			dest.attributes = new AttributeMap(attributes);
 		}
 	}
 
@@ -542,7 +535,7 @@ public class XmlTag
 			return text.text;
 		}
 
-		return toXmlString(null);
+		return toXmlString();
 	}
 
 	/**
@@ -560,10 +553,9 @@ public class XmlTag
 	 * Assuming some attributes have been changed, toXmlString() rebuilds the String on based on the
 	 * tags informations.
 	 * 
-	 * @param attributeToBeIgnored
 	 * @return A xml string matching the tag
 	 */
-	public CharSequence toXmlString(final String attributeToBeIgnored)
+	public CharSequence toXmlString()
 	{
 		final AppendingStringBuffer buffer = new AppendingStringBuffer();
 
@@ -582,31 +574,7 @@ public class XmlTag
 
 		buffer.append(name);
 
-		final IValueMap attributes = getAttributes();
-		if (attributes.size() > 0)
-		{
-			final Iterator<String> iterator = attributes.keySet().iterator();
-			for (; iterator.hasNext();)
-			{
-				final String key = iterator.next();
-				if ((key != null) &&
-					((attributeToBeIgnored == null) || !key.equalsIgnoreCase(attributeToBeIgnored)))
-				{
-					buffer.append(' ');
-					buffer.append(key);
-					CharSequence value = getAttribute(key);
-
-					// Attributes without values are possible, e.g. 'disabled'
-					if (value != null)
-					{
-						buffer.append("=\"");
-						value = Strings.escapeMarkup(value);
-						buffer.append(value);
-						buffer.append('"');
-					}
-				}
-			}
-		}
+		buffer.append(attributes().toCharSequence());
 
 		if (type == TagType.OPEN_CLOSE)
 		{
diff --git a/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java b/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java
index 4598d66..3217738 100644
--- a/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java
@@ -19,6 +19,7 @@ package org.apache.wicket.core.util.string;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.apache.wicket.response.StringResponse;
+import org.apache.wicket.util.value.AttributeMap;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -32,13 +33,19 @@ class CssUtilsTest
 	 * @throws Exception
 	 */
 	@Test
-	void writeLinkUrl() throws Exception
+	void writeLink() throws Exception
 	{
 		StringResponse response = new StringResponse();
 		String url = "some/url;jsessionid=1234?with=parameters&p1=v1";
-		String media = "some&bad&media";
-		CssUtils.writeLinkUrl(response, url, media, "markupId");
-
-		assertEquals("<link rel=\"stylesheet\" type=\"text/css\" href=\"some/url;jsessionid=1234?with=parameters&amp;p1=v1\" media=\"some&amp;bad&amp;media\" id=\"markupId\" />", response.toString());
+		// Media may contain funny things, and it's OK
+		String media = "some&bad&media (min-height: 680px), screen and (orientation: portrait)";
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(CssUtils.ATTR_LINK_REL, "stylesheet");
+		attributes.putAttribute(CssUtils.ATTR_TYPE, "text/css");
+		attributes.putAttribute(CssUtils.ATTR_LINK_HREF, url);
+		attributes.putAttribute(CssUtils.ATTR_LINK_MEDIA, media);
+		attributes.putAttribute(CssUtils.ATTR_ID, "markupId");
+		CssUtils.writeLink(response, attributes);
+		assertEquals("<link rel=\"stylesheet\" type=\"text/css\" href=\"some/url;jsessionid=1234?with=parameters&amp;p1=v1\" media=\"some&amp;bad&amp;media (min-height: 680px), screen and (orientation: portrait)\" id=\"markupId\" />", response.toString());
 	}
 }
diff --git a/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java b/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java
index d89a036..b849101 100644
--- a/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java
@@ -19,6 +19,7 @@ package org.apache.wicket.core.util.string;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.apache.wicket.response.StringResponse;
+import org.apache.wicket.util.value.AttributeMap;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -32,17 +33,19 @@ class JavaScriptUtilsTest
 	 * @throws Exception
 	 */
 	@Test
-	void writeJavaScriptUrl() throws Exception
+	public void writeJavaScript() throws Exception
 	{
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+		attributes.putAttribute(JavaScriptUtils.ATTR_ID, "some\"funny<id&%");
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_DEFER, true);
+		attributes.putAttribute("charset", "some\"funny<charset&%");
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_SRC, "some/url;jsessionid=1234?p1=v1&p2=v2");
 		StringResponse response = new StringResponse();
-		String url = "some/url;jsessionid=1234?p1=v1&p2=v2";
-		String id = "some&bad%id";
-		boolean defer = true;
-		String charset = "some&bad%%charset";
-		JavaScriptUtils.writeJavaScriptUrl(response, url, id, defer, charset);
+		JavaScriptUtils.writeScript(response, attributes);
 
 		assertEquals(
-			"<script type=\"text/javascript\" id=\"some&amp;bad%id\" defer=\"defer\" charset=\"some&amp;bad%%charset\" src=\"some/url;jsessionid=1234?p1=v1&p2=v2\"></script>\n",
+				"<script type=\"text/javascript\" id=\"some&quot;funny&lt;id&amp;%\" defer=\"defer\" charset=\"some&quot;funny&lt;charset&amp;%\" src=\"some/url;jsessionid=1234?p1=v1&amp;p2=v2\"></script>\n",
 			response.toString());
 	}
 
@@ -50,31 +53,35 @@ class JavaScriptUtilsTest
 	 * https://issues.apache.org/jira/browse/WICKET-5715
 	 */
 	@Test
-	void writeJavaScriptUrlAsync()
+	public void writeJavaScriptAsync()
 	{
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+		attributes.putAttribute(JavaScriptUtils.ATTR_ID, "some\"funny<id&%");
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_DEFER, true);
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_ASYNC, true);
+		attributes.putAttribute("charset", "some\"funny<charset&%");
+		attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_SRC, "some/url;jsessionid=1234?p1=v1&p2=v2&p3=v3");
 		StringResponse response = new StringResponse();
-		String url = "some/url;jsessionid=1234?p1=v1&p2=v2";
-		String id = "some&bad%id";
-		boolean defer = true;
-		boolean async = true;
-		String charset = "some&bad%%charset";
-		JavaScriptUtils.writeJavaScriptUrl(response, url, id, defer, charset, async);
+		JavaScriptUtils.writeScript(response, attributes);
 
 		assertEquals(
-				"<script type=\"text/javascript\" id=\"some&amp;bad%id\" defer=\"defer\" async=\"async\" charset=\"some&amp;bad%%charset\" src=\"some/url;jsessionid=1234?p1=v1&p2=v2\"></script>\n",
+				"<script type=\"text/javascript\" id=\"some&quot;funny&lt;id&amp;%\" defer=\"defer\" async=\"async\" charset=\"some&quot;funny&lt;charset&amp;%\" src=\"some/url;jsessionid=1234?p1=v1&amp;p2=v2&amp;p3=v3\"></script>\n",
 				response.toString());
 	}
 
 	/**
 	 */
 	@Test
-	void writeJavaScript()
+	public void writeInlineScript()
 	{
 		StringResponse response = new StringResponse();
-		JavaScriptUtils.writeJavaScript(response,
-			"var message = 'Scripts are written to the <script></script> tag'");
+		AttributeMap attributes = new AttributeMap();
+		attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+		JavaScriptUtils.writeInlineScript(response,
+			"var message = 'Scripts are written to the <script></script> tag'", attributes);
 
-		assertEquals("<script type=\"text/javascript\" >\n" //
+		assertEquals("<script type=\"text/javascript\">\n" //
 			+ "/*<![CDATA[*/\n" //
 			+ "var message = 'Scripts are written to the <script><\\/script> tag'\n" //
 			+ "/*]]>*/\n"//
@@ -84,7 +91,7 @@ class JavaScriptUtilsTest
 	/**
 	 */
 	@Test
-	void scriptTag()
+	public void scriptTag()
 	{
 		assertEquals("<script type=\"text/javascript\">\n/*<![CDATA[*/\n",
 			JavaScriptUtils.SCRIPT_OPEN_TAG);
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.html b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.html
new file mode 100644
index 0000000..ee4a485
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.html
@@ -0,0 +1,4 @@
+<html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd" >
+    <body>
+    </body>
+</html>
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.java
old mode 100755
new mode 100644
similarity index 64%
copy from wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
copy to wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.java
index 8a6f297..2981f2e
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.java
@@ -1,49 +1,47 @@
-/*
- * 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.util.value;
-
-import java.util.Map;
-
-/**
- * <code>ValueMap</code> for attributes.
- * 
- * @author Eelco Hillenius
- * @since 1.2.6
- */
-public final class AttributeMap extends ValueMap
-{
-	private static final long serialVersionUID = 1L;
-
-	/**
-	 * Constructs an empty <code>AttributeMap</code>.
-	 */
-	public AttributeMap()
-	{
-		super();
-	}
-
-	/**
-	 * Copy constructor.
-	 * 
-	 * @param map
-	 *            a <code>Map</code> to be copied
-	 */
-	public AttributeMap(final Map<String, Object> map)
-	{
-		super(map);
-	}
-}
+/*
+ * 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.markup.head.filter;
+
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.WebPage;
+
+/**
+ * @author svenmeier
+ */
+public class CspNoncePage extends WebPage
+{
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * Construct.
+	 * 
+	 * @param parameters
+	 */
+	public CspNoncePage()
+	{
+		add(new AjaxEventBehavior("click")
+		{
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void onEvent(AjaxRequestTarget target)
+			{
+			}
+		});
+	}
+}
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePageExpected.html b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePageExpected.html
new file mode 100644
index 0000000..baa33ba
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePageExpected.html
@@ -0,0 +1,25 @@
+<html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd" >
+    <head><meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' 'nonce-NONCE'; style-src 'nonce-NONCE';" />
+<script type="text/javascript" src="../resource/org.apache.wicket.resource.JQueryResourceReference/jquery/jquery-3.4.1.js" nonce="NONCE"></script>
+<script type="text/javascript" src="../resource/org.apache.wicket.ajax.AbstractDefaultAjaxBehavior/res/js/wicket-ajax-jquery.js" nonce="NONCE"></script>
+<script type="text/javascript" id="wicket-ajax-debug-enable" nonce="NONCE">
+/*<![CDATA[*/
+Wicket.Log.enabled=true;
+/*]]>*/
+</script>
+<script type="text/javascript" id="wicket-ajax-base-url" nonce="NONCE">
+/*<![CDATA[*/
+Wicket.Ajax.baseUrl="wicket/bookmarkable/org.apache.wicket.markup.head.filter.CspNoncePage?0";
+/*]]>*/
+</script>
+<script type="text/javascript" nonce="NONCE">
+/*<![CDATA[*/
+Wicket.Event.add(window, "domready", function(event) { 
+Wicket.Ajax.ajax({"u":"./org.apache.wicket.markup.head.filter.CspNoncePage?0-1.0-","e":"click"});;
+Wicket.Event.publish(Wicket.Event.Topic.AJAX_HANDLERS_BOUND);
+;});
+/*]]>*/
+</script>
+</head><body>
+    </body>
+</html>
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 152dfaf..70ea9d8 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
@@ -94,4 +94,15 @@ class FilteringHeaderResponseTest extends WicketTestCase
 			response -> new ResourceAggregator(new JavaScriptDeferHeaderResponse(response)));
 		executeTest(DeferredPage.class, "DeferredPageExpected.html");
 	}
+	
+	/**
+	 * WICKET-6682
+	 */
+	@Test
+	void nonce() throws Exception
+	{
+		tester.getApplication().setHeaderResponseDecorator(
+			response -> new ResourceAggregator(new CspNonceHeaderResponse(response, "NONCE")));
+		executeTest(CspNoncePage.class, "CspNoncePageExpected.html");
+	}
 }
diff --git a/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java b/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java
index c5a3089..ab82a41 100644
--- a/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java
@@ -48,7 +48,7 @@ class XmlPartialPageUpdateTest extends WicketTestCase
 		
 		update.writeTo(response, "UTF-8");
 		
-		String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ajax-response><component id=\"container1\" ><![CDATA[<span wicket:id=\"container\" id=\"container1\"> two brackets: ]] greater than: > CDATA end: ]]]]><![CDATA[> </span>]]></component><header-contribution><![CDATA[<head xmlns:wicket=\"http://wicket.apache.org\"><script type=\"text/javascript\" >\n" + 
+		String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ajax-response><component id=\"container1\" ><![CDATA[<span wicket:id=\"container\" id=\"container1\"> two brackets: ]] greater than: > CDATA end: ]]]]><![CDATA[> </span>]]></component><header-contribution><![CDATA[<head xmlns:wicket=\"http://wicket.apache.org\"><script type=\"text/javascript\">\n" + 
 				"/*<![CDATA[*/\n" + 
 				"// two brackets: ]] greater than: > CDATA end: ]]]]><![CDATA[>\n" + 
 				"/*]]]]><![CDATA[>*/\n" + 
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/CspApplication.java b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/CspApplication.java
new file mode 100644
index 0000000..b4b8341
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/CspApplication.java
@@ -0,0 +1,72 @@
+/*
+ * 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.examples.csp;
+
+import java.util.Base64;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.Page;
+import org.apache.wicket.Session;
+import org.apache.wicket.examples.WicketExampleApplication;
+import org.apache.wicket.markup.head.ResourceAggregator;
+import org.apache.wicket.markup.head.filter.CspNonceHeaderResponse;
+
+public class CspApplication extends WicketExampleApplication
+{
+	private static final int NONCE_LENGTH = 24;
+	
+	public static MetaDataKey<String> NONCE_KEY = new MetaDataKey<String>()
+	{
+	};
+
+	@Override
+	public Class<? extends Page> getHomePage()
+	{
+		return NonceDemoPage.class;
+	}
+
+	@Override
+	protected void init()
+	{
+		super.init();
+
+		setHeaderResponseDecorator(response -> new ResourceAggregator(new CspNonceHeaderResponse(response, getNonce())));
+		
+		mountPage("noncedemo", NonceDemoPage.class);
+	}
+
+	protected static String generateNonce()
+	{
+		byte[] randomBytes = new byte[NONCE_LENGTH];
+		ThreadLocalRandom.current().nextBytes(randomBytes);
+		return Base64.getUrlEncoder().encodeToString(randomBytes);
+	}
+
+	public static String getNonce()
+	{
+		Session session = Session.get();
+		session.bind();
+		String nonce = session.getMetaData(NONCE_KEY);
+		if (nonce == null)
+		{
+			nonce = generateNonce();
+			session.setMetaData(NONCE_KEY, nonce);
+		}
+		return nonce;
+	}
+}
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.html
new file mode 100644
index 0000000..9115256
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.html
@@ -0,0 +1,34 @@
+<html xmlns:wicket="http://wicket.apache.org" xml:lang="en" lang="en">
+<head>
+    <title>CSP Nonce Demo</title>
+</head>
+<body>
+<wicket:extend>
+    <h3>CSP Nonce Demo</h3>
+    <div wicket:id="testNonceScript" class="test-nonce-script"></div>
+    <div wicket:id="testNoNonceScript" class="test-no-nonce-script"></div>
+    <div class="injected-style"><wicket:message key="styleInjectionContainer"/></div>
+    <div class="injected-style--with-nonce"><wicket:message key="styleInjectionContainerWithNonce"/></div>
+    <p></p>
+    <button wicket:id="clickMe"><wicket:message key="clickMe" /></button>
+    <div class="click-me-text">Click a button above to replace this text</div>
+    <div><wicket:message key="clickMeCount" /> <span wicket:id="clickMeCount"></span></div>
+    <p></p>
+    <p><wicket:message key="ieDisclaimer"/></p>
+
+    <!-- Injections below will work in IE11, because IE 11 doesn't support nonce -->
+
+    <!-- script injection (won't work when CSP nonce is in header) -->
+    <script>document.querySelector(".test-no-nonce-script").innerHTML = "This is text injected by no nonce script: fail";</script>
+
+    <!-- style injection (won't work when CSP nonce is in header) -->
+    <style>
+        .injected-style {
+            color: red;
+            font-weight: bold;
+        }
+    </style>
+
+</wicket:extend>
+</body>
+</html>
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.java
new file mode 100644
index 0000000..d092f8d
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.java
@@ -0,0 +1,81 @@
+/*
+ * 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.examples.csp;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.examples.WicketExamplePage;
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+/**
+ * Page which disallows execution of inline scripts without nonce
+ */
+public class NonceDemoPage extends WicketExamplePage
+{
+	private final IModel<Integer> clickMeCountModel = Model.of(0);
+
+	public NonceDemoPage()
+	{
+		super();
+		add(new Label("testNonceScript", getString("testNonceScript")));
+		add(new Label("testNoNonceScript", getString("testNoNonceScript")));
+		final Label clickMeCount = new Label("clickMeCount", clickMeCountModel);
+		clickMeCount.setOutputMarkupId(true);
+		add(clickMeCount);
+		add(new AjaxLink<String>("clickMe")
+		{
+			@Override
+			public void onClick(AjaxRequestTarget target)
+			{
+				clickMeCountModel.setObject(clickMeCountModel.getObject() + 1);
+
+				// target.add (works even without unsafe-eval)
+				target.add(clickMeCount);
+				// append javascript (won't work without unsafe-eval)
+				target.appendJavaScript("document.querySelector(\".click-me-text\").innerHTML = \"replaced\";");
+			}
+		}.setOutputMarkupId(true));
+	}
+
+	@Override
+	public void renderHead(IHeaderResponse response)
+	{
+		super.renderHead(response);
+		// Add inline script with nonce
+		response.render(JavaScriptHeaderItem.forScript(
+				"$(function(){$(\".test-nonce-script\").html(\"Text injected by script with nonce: success\");});",
+				"test-nonce-script"
+		));
+		// Add inline css with nonce
+		response.render(CssHeaderItem.forCSS(
+				".injected-style--with-nonce{color: green; font-weight: bold;}",
+				"injected-style-with-nonce")
+		);
+	}
+
+	@Override
+	public void detachModels()
+	{
+		super.detachModels();
+		clickMeCountModel.detach();
+	}
+}
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.properties
new file mode 100644
index 0000000..4d9eb01
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.properties
@@ -0,0 +1,21 @@
+#  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.
+testNonceScript = This text should be replaced: fail
+testNoNonceScript = This text should not be replaced: success
+styleInjectionContainer = This text should not be red
+styleInjectionContainerWithNonce = This text should be green
+ieDisclaimer = Results could be different in legacy browsers such as IE. In modern browsers you will experience a few JavaScript errors telling about refused resources.
+clickMe = Click Me
+clickMeCount = Click Me count:
\ No newline at end of file
diff --git a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
index a58b6c1..e0c69bf 100644
--- a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
+++ b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
@@ -52,6 +52,7 @@
 	<tr><td align="right"><a href="bean-validation">Bean Validation</a></td><td> - Bean Validation integration (JSR 303)</td></tr>
        <tr><td align="right"><a href="videos">HTML5 Media Tags</a></td><td> - Integration for HTML5 Media Tags</td></tr>
        <tr><td align="right"><a href="websockets">Web Sockets</a></td><td> - Integration for JSR356 Web Sockets</td></tr>
+       <tr><td align="right"><a href="csp/noncedemo">CSP</a></td><td> - Content Security Protection</td></tr>
    	</tbody>
 	</table>
 </wicket:extend>	
diff --git a/wicket-examples/src/main/webapp/WEB-INF/web.xml b/wicket-examples/src/main/webapp/WEB-INF/web.xml
index e530a05..4364eeb 100644
--- a/wicket-examples/src/main/webapp/WEB-INF/web.xml
+++ b/wicket-examples/src/main/webapp/WEB-INF/web.xml
@@ -258,6 +258,15 @@
 	</filter>
 
 	<filter>
+		<filter-name>CspApplication</filter-name>
+		<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
+		<init-param>
+			<param-name>applicationClassName</param-name>
+			<param-value>org.apache.wicket.examples.csp.CspApplication</param-value>
+		</init-param>
+	</filter>
+
+	<filter>
 		<filter-name>RepeaterExamplesApplication</filter-name>
 		<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
 		<init-param>
@@ -645,6 +654,13 @@
 	</filter-mapping>
 
 	<filter-mapping>
+		<filter-name>CspApplication</filter-name>
+		<url-pattern>/csp/*</url-pattern>
+		<dispatcher>REQUEST</dispatcher>
+		<dispatcher>INCLUDE</dispatcher>
+	</filter-mapping>
+
+	<filter-mapping>
 		<filter-name>FramesApplication</filter-name>
 		<url-pattern>/frames/*</url-pattern>
 	</filter-mapping>
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java b/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
index 8a6f297..d2ccec5 100755
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
+++ b/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
@@ -18,11 +18,16 @@ package org.apache.wicket.util.value;
 
 import java.util.Map;
 
+import org.apache.wicket.util.string.AppendingStringBuffer;
+import org.apache.wicket.util.string.Strings;
+
 /**
- * <code>ValueMap</code> for attributes.
+ * Map of values, extending {@link ValueMap} with methods for generating (HTML) markup attributes.
  * 
  * @author Eelco Hillenius
  * @since 1.2.6
+ * 
+ * @see Strings#escapeMarkup(CharSequence)
  */
 public final class AttributeMap extends ValueMap
 {
@@ -46,4 +51,84 @@ public final class AttributeMap extends ValueMap
 	{
 		super(map);
 	}
+
+	/**
+	 * Put a boolean attribute, removing it if {@code value} is false or using the key as value otherwise,
+	 * i.e. {@code value="value"}. 
+	 * 
+	 * @param key
+	 *            key of attribute
+	 * @param value
+	 * @return previous value
+	 */
+	public boolean putAttribute(final String key, final boolean value)
+	{
+		Object previous = get(key);
+		if (value)
+		{
+			put(key, key);
+		}
+		else
+		{
+			remove(key);
+		}
+		return key.equals(previous);
+	}
+
+	/**
+	 * Put a string attribute, removing it if the string is empty (nor null).
+	 * 
+	 * @param key
+	 *            key of attribute
+	 * @param value
+	 * @return previous value
+	 */
+	public String putAttribute(String key, CharSequence value)
+	{
+		if (Strings.isEmpty(value))
+		{
+			return (String)remove(key);
+		}
+		else
+		{
+			return (String)put(key, value);
+		}
+	}
+
+	/**
+	 * Representation as encoded markup attributes.
+	 * 
+	 * @see Strings#escapeMarkup(CharSequence)
+	 */
+	public String toString()
+	{
+		return toCharSequence().toString();
+	}
+
+	/**
+	 * Representation as encoded markup attributes.
+	 * 
+	 * @see Strings#escapeMarkup(CharSequence)
+	 */
+	public CharSequence toCharSequence()
+	{
+		final AppendingStringBuffer buffer = new AppendingStringBuffer();
+
+		for (String key : keySet())
+		{
+			if (key != null) {
+				buffer.append(' ');
+				buffer.append(Strings.escapeMarkup(key));
+				
+				CharSequence value = getCharSequence(key);
+				if (value != null) {
+					buffer.append("=\"");
+					buffer.append(Strings.escapeMarkup(value));
+					buffer.append('"');
+				}
+			}
+		}
+
+		return buffer;
+	}
 }
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java b/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java
index c0c5e00..ae554ae 100755
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java
+++ b/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java
@@ -28,6 +28,7 @@ import java.util.Map;
 
 import org.apache.wicket.util.parse.metapattern.MetaPattern;
 import org.apache.wicket.util.parse.metapattern.parsers.VariableAssignmentParser;
+import org.apache.wicket.util.string.AppendingStringBuffer;
 import org.apache.wicket.util.string.IStringIterator;
 import org.apache.wicket.util.string.StringList;
 import org.apache.wicket.util.string.StringValue;
@@ -544,7 +545,7 @@ public class ValueMap extends LinkedHashMap<String, Object> implements IValueMap
 	@Override
 	public String toString()
 	{
-		final StringBuilder buffer = new StringBuilder();
+		final AppendingStringBuffer buffer = new AppendingStringBuffer();
 		boolean first = true;
 		for (Map.Entry<String, Object> entry : entrySet())
 		{
diff --git a/wicket-util/src/test/java/org/apache/wicket/util/value/AttributeMapTest.java b/wicket-util/src/test/java/org/apache/wicket/util/value/AttributeMapTest.java
new file mode 100644
index 0000000..e010655
--- /dev/null
+++ b/wicket-util/src/test/java/org/apache/wicket/util/value/AttributeMapTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.util.value;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for {@link AttributeMap}.
+ * 
+ * @author svenmeier
+ */
+public class AttributeMapTest
+{
+
+	@Test
+	public void encoding() {
+		AttributeMap map = new AttributeMap();
+		
+		map.putAttribute("key", "-&-ö-<->-");
+		
+		assertEquals(" key=\"-&amp;-ö-&lt;-&gt;-\"", map.toString());
+	}
+	
+	@Test
+	public void putString()
+	{
+		AttributeMap map = new AttributeMap();
+
+		assertNull(map.putAttribute("foo", "bar"));
+		assertEquals("bar", map.get("foo"));
+
+		assertEquals("bar", map.putAttribute("foo", "baz"));
+		assertEquals("baz", map.get("foo"));
+
+		assertEquals("baz", map.putAttribute("foo", null));
+		assertEquals(null, map.get("foo"));
+	}
+
+	@Test
+	public void putBoolean()
+	{
+		AttributeMap map = new AttributeMap();
+
+		assertFalse(map.putAttribute("foo", true));
+		assertEquals("foo", map.get("foo"));
+
+		assertTrue(map.putAttribute("foo", false));
+		assertEquals(null, map.get("foo"));
+	}
+}
diff --git a/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java b/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java
index be3d6b7..4aad486 100644
--- a/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java
+++ b/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java
@@ -244,6 +244,9 @@ public class ValueMapTest
 		assertEquals(defDuration, vm.getAsDuration("duration.bad", defDuration));
 		assertNull(vm.getAsDuration("duration.missing"));
 		assertEquals(defDuration, vm.getAsDuration("duration.missing", defDuration));
+		
+		vm.remove("instant");
+		assertEquals("num = \"42\" num.bad = \"xxx\" instant.bad = \"xxx\" duration = \"PT1H\" duration.bad = \"xxx\" boolean = \"true\" boolean.bad = \"xxx\"", vm.toString());
 	}
 
 	/**