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

(wicket) 01/01: [WICKET-7142] add a CSRF token-generating-including-checking form

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

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

commit ca082f9f376206d8ae56e6d60d3ac7e0ab94c800
Author: reiern70 <re...@gmail.com>
AuthorDate: Tue Aug 5 14:20:26 2025 -0500

    [WICKET-7142] add a CSRF token-generating-including-checking form
---
 .../apache/wicket/markup/html/form/CsrfForm.java   | 137 +++++++++++++++++++++
 1 file changed, 137 insertions(+)

diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/CsrfForm.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/CsrfForm.java
new file mode 100644
index 0000000000..b44ceacc19
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/CsrfForm.java
@@ -0,0 +1,137 @@
+package org.apache.wicket.markup.html.form;
+
+import java.util.UUID;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.MarkupStream;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.LambdaModel;
+import org.apache.wicket.util.string.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * A {@link Form} that adds cross-site request forgery protection.
+ *
+ * @param <T>
+ *            The model object type
+ */
+public class CsrfForm<T> extends Form<T> {
+
+	private static final long serialVersionUID = 1L;
+
+	// we can't use LogCategory as it is not visible here.
+	private static final Logger LOGGER = LoggerFactory.getLogger(CsrfForm.class);
+
+	public static class CSRFViolationException extends WicketRuntimeException {
+
+		private static final long serialVersionUID = 1L;
+
+		public CSRFViolationException(String message) {
+			super(message);
+		}
+	}
+
+	// serves as a mean to transfer CSRF protection token between server and client (and vice versa)
+	private String clientCSRFToken;
+	private final String serverCSRFToken;
+
+	private HiddenField<String> csrfTokenHiddenField;
+
+	/**
+	 * Constructs a CsrfForm
+	 *
+	 * @param id See Component
+	 */
+	public CsrfForm(String id)
+	{
+		super(id);
+		clientCSRFToken = generateCSRFToken();
+		// keep an immutable copy on the server side.
+		serverCSRFToken = clientCSRFToken;
+	}
+
+	/**
+	 * Constructs a CsrfForm
+	 *
+	 * @param id
+	 *            See Component
+	 * @param model
+	 *            See Component
+	 * @see org.apache.wicket.Component#Component(String, IModel)
+	 */
+	public CsrfForm(String id, IModel<T> model)
+	{
+		super(id, model);
+		clientCSRFToken = generateCSRFToken();
+		// keep an immutable copy on the server side.
+		serverCSRFToken = clientCSRFToken;
+	}
+
+	/**
+	 * Override to use a different way to generate a token. Default uses
+	 * {@link UUID#randomUUID()}
+	 *
+	 * @return a CSRF token
+	 */
+	protected String generateCSRFToken()
+	{
+		return UUID.randomUUID().toString();
+	}
+
+	@Override
+	protected void onInitialize()
+	{
+		super.onInitialize();
+		// the client will submit clientCSRFToken, and it will be set in clientCSRFToken on the server side
+		// initially server sets this value, and it is expecting it back, unchanged, from the client along with
+		// submitted data
+		csrfTokenHiddenField = new HiddenField<>("CRSFFormVersion", LambdaModel.of(CsrfForm.this::getClientCSRFToken, CsrfForm.this::setClientCSRFToken), String.class);
+		add(csrfTokenHiddenField);
+	}
+
+	@Override
+	protected void delegateSubmit(IFormSubmitter submittingComponent) {
+		// we compare the version stored on the server with the version the client sent
+		// form is only processed if they match
+		if (serverCSRFToken.equals(clientCSRFToken))
+		{
+			super.delegateSubmit(submittingComponent);
+		}
+		else
+		{
+			onCSRFViolation(clientCSRFToken, serverCSRFToken);
+		}
+	}
+
+	/**
+	 * Called when server stored token and the client sent token do not match.
+	 *
+	 * @param clientCSRFToken The client submitted token
+	 * @param serverCSRFToken The server stored token.
+	 */
+	protected void onCSRFViolation(String clientCSRFToken, String serverCSRFToken)
+	{
+		String message = String.format("Preventing possible CSRF attack! Client clientCSRFToken='%s' and server serverCSRFToken='%s' do not match!", clientCSRFToken, serverCSRFToken);
+		LOGGER.error(message);
+		throw new CSRFViolationException(message);
+	}
+
+	@Override
+	public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag)
+	{
+		getResponse().write("<input type=\"hidden\" class=\"csrf-form\" name=\"" + csrfTokenHiddenField.getInputName() + "\" value =\"" + Strings.escapeMarkup(serverCSRFToken) + "\"/>");
+		super.onComponentTagBody(markupStream, openTag);
+	}
+
+	public String getClientCSRFToken()
+	{
+		return clientCSRFToken;
+	}
+
+	public void setClientCSRFToken(String clientCSRFToken)
+	{
+		this.clientCSRFToken = clientCSRFToken;
+	}
+}
\ No newline at end of file