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