From 1cce283aca84e59c0ac401f0ae0e32a12c34fc60 Mon Sep 17 00:00:00 2001 From: ziggy Date: Fri, 10 Jul 2026 17:48:12 +0300 Subject: [PATCH] Expose invalid envelope content in receive output --- .../api/InvalidEnvelopeContentException.java | 54 +++++++++++++++ .../signal/manager/api/MessageEnvelope.java | 4 ++ .../helper/IncomingMessageHandler.java | 69 ++++++++++++++++++- .../helper/IncomingMessageHandlerTest.java | 40 +++++++++++ .../java/org/asamk/signal/json/JsonError.java | 22 +++++- .../org/asamk/signal/json/JsonErrorTest.java | 50 ++++++++++++++ 6 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/InvalidEnvelopeContentException.java create mode 100644 lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java create mode 100644 src/test/java/org/asamk/signal/json/JsonErrorTest.java diff --git a/lib/src/main/java/org/asamk/signal/manager/api/InvalidEnvelopeContentException.java b/lib/src/main/java/org/asamk/signal/manager/api/InvalidEnvelopeContentException.java new file mode 100644 index 00000000..0ecabb35 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/InvalidEnvelopeContentException.java @@ -0,0 +1,54 @@ +package org.asamk.signal.manager.api; + +import java.util.List; + +public final class InvalidEnvelopeContentException extends Exception { + + public static final String INVALID_ENVELOPE_CONTENT = "INVALID_ENVELOPE_CONTENT"; + public static final String DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS = "DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS"; + + private final String code; + private final String sender; + private final int senderDevice; + private final Integer bodyLength; + private final List invalidBodyRanges; + + public InvalidEnvelopeContentException( + final String message, + final String code, + final String sender, + final int senderDevice, + final Integer bodyLength, + final List invalidBodyRanges, + final Throwable cause + ) { + super(message, cause); + this.code = code; + this.sender = sender; + this.senderDevice = senderDevice; + this.bodyLength = bodyLength; + this.invalidBodyRanges = List.copyOf(invalidBodyRanges); + } + + public String getCode() { + return code; + } + + public String getSender() { + return sender; + } + + public int getSenderDevice() { + return senderDevice; + } + + public Integer getBodyLength() { + return bodyLength; + } + + public List getInvalidBodyRanges() { + return invalidBodyRanges; + } + + public record InvalidBodyRange(int index, Integer start, Integer length, String type) {} +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index 34b2274d..6b2c59da 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.helper.RecipientAddressResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.MimeUtils; +import org.signal.core.models.ServiceId; import org.signal.libsignal.metadata.ProtocolException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -1032,6 +1033,8 @@ public record MessageEnvelope( ? recipientResolver.resolveRecipient(serviceId) : envelope.isUnidentifiedSender() && content != null ? recipientResolver.resolveRecipient(content.getSender()) + : exception instanceof InvalidEnvelopeContentException e && e.getSender() != null + ? recipientResolver.resolveRecipient(ServiceId.parseOrThrow(e.getSender())) : exception instanceof ProtocolException e ? recipientResolver.resolveRecipient(e.getSender()) : null; @@ -1039,6 +1042,7 @@ public record MessageEnvelope( ? envelope.getSourceDevice() : content != null ? content.getSenderDevice() + : exception instanceof InvalidEnvelopeContentException e ? e.getSenderDevice() : exception instanceof ProtocolException e ? e.getSenderDevice() : 0; Optional receipt; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 5069749c..62545c52 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -21,6 +21,7 @@ import org.asamk.signal.manager.actions.SyncStorageDataAction; import org.asamk.signal.manager.actions.UpdateAccountAttributesAction; import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupNotFoundException; +import org.asamk.signal.manager.api.InvalidEnvelopeContentException; import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.ReceiveConfig; @@ -52,6 +53,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.InvalidMessageStructureException; +import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata; import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder; import org.whispersystems.signalservice.api.crypto.SignalServiceCipherResult; import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator; @@ -69,6 +71,8 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.push.BodyRange; +import org.whispersystems.signalservice.internal.push.Content; import org.whispersystems.signalservice.internal.push.Envelope; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; @@ -225,7 +229,8 @@ public final class IncomingMessageHandler { Envelope envelope, SignalServiceCipherResult cipherResult, long serverDeliveredTimestamp - ) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException { + ) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, + InvalidMessageStructureException, InvalidEnvelopeContentException { final var content = cipherResult.getContent(); final var envelopeMetadata = cipherResult.getMetadata(); final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope, @@ -234,8 +239,17 @@ public final class IncomingMessageHandler { cipherResult.getMetadata().getCiphertextMessageType()); if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) { - logger.warn("Invalid content! {}", v.getReason(), v.getThrowable()); - return null; + final var exception = createInvalidEnvelopeContentException(v, envelopeMetadata, content); + logger.warn("Invalid content! reason={} code={} source={} sourceDevice={} timestamp={} bodyLength={} invalidBodyRanges={}", + exception.getMessage(), + exception.getCode(), + exception.getSender(), + exception.getSenderDevice(), + envelope.clientTimestamp, + exception.getBodyLength(), + exception.getInvalidBodyRanges()); + logger.debug("Invalid content validation location", v.getThrowable()); + throw exception; } if (validationResult instanceof EnvelopeContentValidator.Result.UnsupportedDataMessage v) { @@ -252,6 +266,55 @@ public final class IncomingMessageHandler { serverDeliveredTimestamp); } + static InvalidEnvelopeContentException createInvalidEnvelopeContentException( + final EnvelopeContentValidator.Result.Invalid validationResult, + final EnvelopeMetadata envelopeMetadata, + final Content content + ) { + final var dataMessage = content.dataMessage; + final Integer bodyLength = dataMessage == null + ? null + : dataMessage.body == null ? 0 : dataMessage.body.length(); + final List invalidBodyRanges; + if (dataMessage == null) { + invalidBodyRanges = List.of(); + } else { + invalidBodyRanges = new ArrayList<>(); + for (int i = 0; i < dataMessage.bodyRanges.size(); i++) { + final var range = dataMessage.bodyRanges.get(i); + final long start = range.start == null ? 0 : range.start; + final long length = range.length == null ? 0 : range.length; + if (start < 0 || length < 0 || start + length > bodyLength) { + invalidBodyRanges.add(new InvalidEnvelopeContentException.InvalidBodyRange(i, + range.start, + range.length, + getBodyRangeType(range))); + } + } + } + + final var code = "[DataMessage] Body range with out-of-bounds start/length!".equals(validationResult.getReason()) + ? InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS + : InvalidEnvelopeContentException.INVALID_ENVELOPE_CONTENT; + return new InvalidEnvelopeContentException(validationResult.getReason(), + code, + envelopeMetadata.getSourceServiceId().toString(), + envelopeMetadata.getSourceDeviceId(), + bodyLength, + invalidBodyRanges, + validationResult.getThrowable()); + } + + private static String getBodyRangeType(final BodyRange range) { + if (range.style != null) { + return "STYLE_" + range.style.name(); + } + if (range.mentionAci != null || range.mentionAciBinary != null) { + return "MENTION"; + } + return "UNKNOWN"; + } + private List checkAndHandleMessage( final SignalServiceEnvelope envelope, final SignalServiceContent content, diff --git a/lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java b/lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java new file mode 100644 index 00000000..33ecee72 --- /dev/null +++ b/lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java @@ -0,0 +1,40 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.api.InvalidEnvelopeContentException; +import org.junit.jupiter.api.Test; +import org.signal.core.models.ServiceId.ACI; +import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata; +import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator; +import org.whispersystems.signalservice.internal.push.BodyRange; +import org.whispersystems.signalservice.internal.push.Content; +import org.whispersystems.signalservice.internal.push.DataMessage; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class IncomingMessageHandlerTest { + + @Test + void invalidEnvelopeContentReportsOutOfBoundsBodyRange() { + final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de"); + final var bodyRange = new BodyRange.Builder().start(4).length(3).style(BodyRange.Style.BOLD).build(); + final var dataMessage = new DataMessage.Builder().body("hello").bodyRanges(List.of(bodyRange)).build(); + final var content = new Content.Builder().dataMessage(dataMessage).build(); + final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1); + final var validationResult = new EnvelopeContentValidator.Result.Invalid( + "[DataMessage] Body range with out-of-bounds start/length!", + new Throwable()); + + final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult, + metadata, + content); + + assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode()); + assertEquals(sender.toString(), exception.getSender()); + assertEquals(2, exception.getSenderDevice()); + assertEquals(5, exception.getBodyLength()); + assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, 4, 3, "STYLE_BOLD")), + exception.getInvalidBodyRanges()); + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonError.java b/src/main/java/org/asamk/signal/json/JsonError.java index d881c0b7..bc6e4254 100644 --- a/src/main/java/org/asamk/signal/json/JsonError.java +++ b/src/main/java/org/asamk/signal/json/JsonError.java @@ -1,11 +1,29 @@ package org.asamk.signal.json; +import com.fasterxml.jackson.annotation.JsonInclude; import io.micronaut.jsonschema.JsonSchema; +import org.asamk.signal.manager.api.InvalidEnvelopeContentException; + +import java.util.List; + @JsonSchema(title = "Error") -public record JsonError(String message, String type) { +public record JsonError( + String message, + String type, + @JsonInclude(JsonInclude.Include.NON_NULL) Details details +) { public static JsonError from(Throwable exception) { - return new JsonError(exception.getMessage(), exception.getClass().getSimpleName()); + final var details = exception instanceof InvalidEnvelopeContentException e + ? new Details(e.getCode(), e.getBodyLength(), e.getInvalidBodyRanges()) + : null; + return new JsonError(exception.getMessage(), exception.getClass().getSimpleName(), details); } + + public record Details( + String code, + Integer bodyLength, + List invalidBodyRanges + ) {} } diff --git a/src/test/java/org/asamk/signal/json/JsonErrorTest.java b/src/test/java/org/asamk/signal/json/JsonErrorTest.java new file mode 100644 index 00000000..8f3e43f0 --- /dev/null +++ b/src/test/java/org/asamk/signal/json/JsonErrorTest.java @@ -0,0 +1,50 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.api.InvalidEnvelopeContentException; +import org.asamk.signal.util.Util; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JsonErrorTest { + + @Test + void invalidEnvelopeContentIncludesStructuredDetails() throws Exception { + final var invalidRange = new InvalidEnvelopeContentException.InvalidBodyRange(2, 8, 4, "STYLE_BOLD"); + final var exception = new InvalidEnvelopeContentException("invalid body range", + InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, + null, + 3, + 10, + List.of(invalidRange), + new Throwable()); + + final var error = JsonError.from(exception); + + assertEquals("invalid body range", error.message()); + assertEquals("InvalidEnvelopeContentException", error.type()); + assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, + error.details().code()); + assertEquals(10, error.details().bodyLength()); + assertEquals(List.of(invalidRange), error.details().invalidBodyRanges()); + final var json = Util.createJsonObjectMapper().writeValueAsString(error); + assertTrue(json.contains("\"code\":\"DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS\"")); + assertTrue(json.contains("\"start\":8")); + } + + @Test + void ordinaryExceptionDoesNotIncludeDetails() throws Exception { + final var error = JsonError.from(new IllegalArgumentException("bad argument")); + + assertEquals("bad argument", error.message()); + assertEquals("IllegalArgumentException", error.type()); + assertNull(error.details()); + final var json = Util.createJsonObjectMapper().writeValueAsString(error); + assertFalse(json.contains("details")); + } +}