diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java index 3af1befa7..6969c5e68 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java @@ -209,6 +209,11 @@ public class MessageController { @VisibleForTesting static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes(); + // The Signal desktop client (really, JavaScript in general) can handle message timestamps at most 100,000,000 days + // past the epoch; please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date + // for additional details. + public static final long MAX_TIMESTAMP = 86_400_000L * 100_000_000L; + private static final Duration NOTIFY_FOR_REMAINING_MESSAGES_DELAY = Duration.ofMinutes(1); public MessageController( @@ -554,6 +559,10 @@ public class MessageController { @Context ContainerRequestContext context) throws RateLimitExceededException { + if (timestamp < 0 || timestamp > MAX_TIMESTAMP) { + throw new BadRequestException("Illegal timestamp"); + } + final SpamChecker.SpamCheckResult spamCheck = spamChecker.checkForSpam(context, Optional.empty(), Optional.empty(), Optional.empty()); if (spamCheck instanceof final SpamChecker.Spam spam) { return spam.response(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java index 1368c603d..440b50fcc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java @@ -9,6 +9,8 @@ import static com.codahale.metrics.MetricRegistry.name; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.PositiveOrZero; import org.whispersystems.textsecuregcm.controllers.MessageController; import io.micrometer.core.instrument.Counter; @@ -19,8 +21,17 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; -public record IncomingMessageList(@NotNull @Valid List<@NotNull IncomingMessage> messages, - boolean online, boolean urgent, long timestamp) { +public record IncomingMessageList(@NotNull + @Valid + List<@NotNull @Valid IncomingMessage> messages, + + boolean online, + + boolean urgent, + + @PositiveOrZero + @Max(MessageController.MAX_TIMESTAMP) + long timestamp) { private static final Counter REJECT_DUPLICATE_RECIPIENT_COUNTER = Metrics.counter( diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java index 6f255fb2b..f570efc1d 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java @@ -1095,7 +1095,7 @@ class MessageControllerTest { .request() .header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)) .put(Entity.entity(new IncomingMessageList( - List.of(new IncomingMessage(1, (byte) 1, 1, new String(contentBytes))), false, true, + List.of(new IncomingMessage(1, (byte) 1, 1, Base64.getEncoder().encodeToString(contentBytes))), false, true, System.currentTimeMillis()), MediaType.APPLICATION_JSON_TYPE))) { @@ -1599,7 +1599,7 @@ class MessageControllerTest { @ParameterizedTest @CsvSource({ - "-1, 422", + "-1, 400", "0, 200", "1, 200", "8640000000000000, 200",