Защита от перебора паролей: SMTP, IMAP, веб — лимит по IP и по ящику
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
Один LoginGuard на все проверки пароля (SMTP AUTH, IMAP LOGIN/AUTHENTICATE, вход в веб, смена пароля): 10 неверных с IP за 15 мин → блок 15 мин, дальше вдвое дольше до суток; 20 неверных на ящик с разных IP → ящик закрыт, кроме IP, с которых в него уже входили. Во время блока пароль не проверяется (SMTP 421, IMAP BYE + NO [UNAVAILABLE], веб 429 + Retry-After); неверный пароль — ответ через 1 с; 3 неверных в соединении — обрыв. X-Forwarded-For веб-входа — только от прокси в частной сети, иначе заголовок отбрасывается (подменой IP лимит не обойти). Попутно: IMAP терял тегированный ответ после "* BYE" (LOGOUT, обрыв после неудачных входов) — takeUntil теперь после тегированной строки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.mailserver.smtp;
|
||||
|
||||
import com.mailserver.security.LoginGuard;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -54,6 +55,8 @@ public class SmtpServer {
|
||||
static final int MAX_LINE = 64 * 1024;
|
||||
static final int MAX_RECIPIENTS = 100;
|
||||
private static final int MAX_BAD_COMMANDS = 20;
|
||||
/** Wrong passwords in one connection before it is closed. */
|
||||
private static final int MAX_AUTH_FAILURES = 3;
|
||||
// Compared by identity: TLS starts right after exactly this reply is written
|
||||
private static final String STARTTLS_READY = new String("220 2.0.0 Ready to start TLS\r\n");
|
||||
|
||||
@@ -84,11 +87,17 @@ public class SmtpServer {
|
||||
|
||||
private final Settings settings;
|
||||
private final SmtpBackend backend;
|
||||
private final LoginGuard guard;
|
||||
private DisposableServer server;
|
||||
|
||||
public SmtpServer(Settings settings, SmtpBackend backend) {
|
||||
this(settings, backend, LoginGuard.disabled());
|
||||
}
|
||||
|
||||
public SmtpServer(Settings settings, SmtpBackend backend, LoginGuard guard) {
|
||||
this.settings = settings;
|
||||
this.backend = backend;
|
||||
this.guard = guard;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -183,6 +192,7 @@ public class SmtpServer {
|
||||
final ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
boolean dataTooBig;
|
||||
int badCommands;
|
||||
int authFailures;
|
||||
volatile boolean closing;
|
||||
|
||||
Session(String remoteIp, boolean tls) {
|
||||
@@ -356,16 +366,32 @@ public class SmtpServer {
|
||||
|
||||
private Mono<String> checkPassword(String user, String password) {
|
||||
String username = user.trim().toLowerCase(Locale.ROOT);
|
||||
Duration wait = guard.retryAfter(remoteIp, username);
|
||||
if (!wait.isZero()) {
|
||||
// Blocked: the password is not even checked, the connection is closed
|
||||
log.warn("SMTP {}: login as {} refused, blocked for {}", remoteIp, username, LoginGuard.human(wait));
|
||||
closing = true;
|
||||
return reply("421 4.7.0 Too many failed logins, try again in " + LoginGuard.human(wait));
|
||||
}
|
||||
return blocking(() -> backend.authenticate(username, password))
|
||||
.map(ok -> {
|
||||
.flatMap(ok -> {
|
||||
if (ok) {
|
||||
guard.success(remoteIp, username);
|
||||
authenticatedUser = username;
|
||||
log.info("SMTP {}: authenticated as {}", remoteIp, username);
|
||||
return "235 2.7.0 Authentication successful\r\n";
|
||||
return Mono.just("235 2.7.0 Authentication successful\r\n");
|
||||
}
|
||||
guard.failure(remoteIp, username);
|
||||
log.warn("SMTP {}: authentication failed for {}", remoteIp, username);
|
||||
badCommands++;
|
||||
return "535 5.7.8 Authentication credentials invalid\r\n";
|
||||
if (++authFailures >= MAX_AUTH_FAILURES) {
|
||||
closing = true;
|
||||
}
|
||||
String answer = closing
|
||||
? "535 5.7.8 Authentication credentials invalid, closing connection\r\n"
|
||||
: "535 5.7.8 Authentication credentials invalid\r\n";
|
||||
Duration delay = guard.failureDelay();
|
||||
return delay.isZero() ? Mono.just(answer) : Mono.just(answer).delayElement(delay);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user