From 7f77cf0268e874235e0cd03e8b56b6543fe1b38a Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 17 Aug 2026 22:18:59 +0200 Subject: [PATCH] prevent Host header injection attacks --- .../src/main/java/com/commafeed/CommaFeedApplication.java | 5 +++++ .../main/java/com/commafeed/CommaFeedConfiguration.java | 7 +++++++ .../java/com/commafeed/frontend/resource/UserREST.java | 3 ++- commafeed-server/src/main/resources/application.properties | 1 + .../test/java/com/commafeed/integration/rest/UserIT.java | 2 ++ 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java index 7a137a84..e01cdfe1 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java @@ -41,6 +41,11 @@ public class CommaFeedApplication { ImageProxyUrl.generateKey(); } + if (config.passwordRecoveryEnabled() && config.passwordRecoveryPublicBaseUrl().isEmpty()) { + throw new IllegalStateException( + "password recovery is enabled but no public base url is set"); + } + feedRefreshEngine.start(); taskScheduler.start(); } diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 9ac75fd2..8d990d2e 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -49,6 +49,13 @@ public interface CommaFeedConfiguration { @WithDefault("false") boolean passwordRecoveryEnabled(); + /** + * The base URL of the application to use in the password recovery link in the email. We can't + * use the URL sent by the browser of the user because of malicious Host header injection + * attacks. + */ + Optional passwordRecoveryPublicBaseUrl(); + /** Message displayed in a notification at the bottom of the page. */ Optional announcement(); 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 6d517cba..eb9420e4 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 @@ -413,7 +413,8 @@ public class UserREST { } private String buildEmailContent(User user) throws URISyntaxException { - String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString()); + String publicUrl = + 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)); diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties index 8c52de33..dbb8e1db 100644 --- a/commafeed-server/src/main/resources/application.properties +++ b/commafeed-server/src/main/resources/application.properties @@ -52,6 +52,7 @@ quarkus.native.additional-build-args=-H:PageSize=65536 %test.commafeed.users.create-demo-account=true %test.commafeed.users.allow-registrations=true %test.commafeed.password-recovery-enabled=true +%test.commafeed.password-recovery-public-base-url=https://commafeed.example.com %test.commafeed.http-client.cache.enabled=false %test.commafeed.http-client.block-local-addresses=false %test.commafeed.database.cleanup.entries-max-age=0 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 5db0ce1d..22a38a9d 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 @@ -52,6 +52,7 @@ class UserIT extends BaseIT { RestAssured.given() .body(req) .contentType(ContentType.JSON) + .header("Host", "malicious.url.com") .post("rest/user/passwordReset") .then() .statusCode(200); @@ -64,6 +65,7 @@ class UserIT extends BaseIT { Assertions.assertTrue( message.getHtml() .startsWith("You asked for password recovery for account 'admin'")); + Assertions.assertTrue(message.getHtml().contains("https://commafeed.example.com")); Assertions.assertEquals("admin@commafeed.com", message.getTo().getFirst()); Element a = Jsoup.parse(message.getHtml()).select("a").getFirst();