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:40 UTC

(wicket) branch reiern70/crsf created (now ca082f9f37)

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

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


      at ca082f9f37 [WICKET-7142] add a CSRF token-generating-including-checking form

This branch includes the following new commits:

     new ca082f9f37 [WICKET-7142] add a CSRF token-generating-including-checking form

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



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

Posted by re...@apache.org.
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