Expose invalid envelope content in receive output

This commit is contained in:
ziggy 2026-07-10 17:48:12 +03:00
parent 98d9960e66
commit 1cce283aca
6 changed files with 234 additions and 5 deletions

View File

@ -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<InvalidBodyRange> invalidBodyRanges;
public InvalidEnvelopeContentException(
final String message,
final String code,
final String sender,
final int senderDevice,
final Integer bodyLength,
final List<InvalidBodyRange> 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<InvalidBodyRange> getInvalidBodyRanges() {
return invalidBodyRanges;
}
public record InvalidBodyRange(int index, Integer start, Integer length, String type) {}
}

View File

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

View File

@ -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<InvalidEnvelopeContentException.InvalidBodyRange> 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<HandleAction> checkAndHandleMessage(
final SignalServiceEnvelope envelope,
final SignalServiceContent content,

View File

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

View File

@ -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<InvalidEnvelopeContentException.InvalidBodyRange> invalidBodyRanges
) {}
}

View File

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