From ddff408afe45cf0eccc01d098f80aa56e20ad939 Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 2 Nov 2026 12:34:14 +0100 Subject: [PATCH] escape username in password recovery email --- .../commafeed/frontend/resource/UserREST.java | 3 +- .../commafeed/integration/rest/UserIT.java | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java index 96b40c78..4a82193a 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java @@ -35,6 +35,7 @@ import com.commafeed.frontend.model.request.RegistrationRequest; import com.commafeed.security.AuthenticationContext; import com.commafeed.security.Roles; import com.google.common.base.Preconditions; +import com.google.common.html.HtmlEscapers; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; @@ -412,7 +413,7 @@ public class UserREST { Urls.removeTrailingSlash(config.passwordRecoveryPublicBaseUrl().orElseThrow()); return String.format( "You asked for password recovery for account '%s', follow this link to change your password. Ignore this if you didn't request a password recovery.", - user.getName(), callbackUrl(user, publicUrl)); + HtmlEscapers.htmlEscaper().escape(user.getName()), callbackUrl(user, publicUrl)); } private String callbackUrl(User user, String publicUrl) throws URISyntaxException { diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java index 219c84c5..8b4eb280 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java @@ -4,6 +4,7 @@ import com.commafeed.TestConstants; import com.commafeed.frontend.model.Settings; import com.commafeed.frontend.model.request.PasswordResetConfirmationRequest; import com.commafeed.frontend.model.request.PasswordResetRequest; +import com.commafeed.frontend.model.request.RegistrationRequest; import com.commafeed.integration.BaseIT; import io.quarkus.mailer.MockMailbox; @@ -17,6 +18,7 @@ import jakarta.ws.rs.core.HttpHeaders; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -119,6 +121,45 @@ class UserIT extends BaseIT { RestAssured.given().get("rest/user/settings").then().statusCode(200); } + @Test + void resetPasswordEscapesUsernameInEmail() { + String name = "CLICK"; + String email = "victim@commafeed.com"; + + RegistrationRequest registrationReq = new RegistrationRequest(); + registrationReq.setName(name); + registrationReq.setPassword("MyPassword123!"); + registrationReq.setEmail(email); + RestAssured.given() + .body(registrationReq) + .contentType(ContentType.JSON) + .post("rest/user/register") + .then() + .statusCode(200); + + PasswordResetRequest req = new PasswordResetRequest(); + req.setEmail(email); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/user/passwordReset") + .then() + .statusCode(200); + + List mails = mailbox.getMailMessagesSentTo(email); + Assertions.assertEquals(1, mails.size()); + + String html = mails.getFirst().getHtml(); + Assertions.assertTrue( + html.startsWith( + "You asked for password recovery for account '<a href=//evil.example>CLICK</a>'")); + + Elements links = Jsoup.parse(html).select("a"); + Assertions.assertEquals(1, links.size()); + Assertions.assertTrue( + links.getFirst().attr("href").startsWith("https://commafeed.example.com")); + } + @Test void saveSettings() { Settings settings =