prevent Host header injection attacks

This commit is contained in:
Athou
2026-08-17 22:18:59 +02:00
parent f3c390e0e0
commit 7f77cf0268
5 changed files with 17 additions and 1 deletions

View File

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

View File

@@ -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<String> passwordRecoveryPublicBaseUrl();
/** Message displayed in a notification at the bottom of the page. */
Optional<String> announcement();

View File

@@ -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', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
user.getName(), callbackUrl(user, publicUrl));

View File

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

View File

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