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 <reier...@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

Reply via email to