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();