Skip to content

Forwarded message sends the WRONG attachment after changing the sender #13177

Description

@hermann1505

Steps to reproduce

Forward an email that has attachments, then change the sender (alias) before sending.
The message is sent with a different attachment than the one displayed — a file
the user never selected, attached, or downloaded. This is a data-confidentiality
issue: the wrong file (potentially another message's attachment) can be transmitted
to the recipient.

  1. Open a received email that has one or more attachments.
  2. Forward it. The original attachments are shown attached to the forward.
  3. Change the From / sender alias on the compose form.
  4. Send.
  5. The sent message (check Sent folder) carries a different attachment than the one shown in the composer.

Expected behavior

The correct Attachment(s) should be sent

Actual behavior

Observed on this instance (server-side evidence)

From nextcloud.log, a forward (subject "Fwd: …", account_id 13) during an active
sender change:

Time (UTC) Request Result
12:30:41 POST /api/drafts InvalidArgumentException: Attachment does not have a type (LocalMessage 131)
12:31:10 POST /api/drafts same error (LocalMessage 132)

The draft auto-save threw (same type-missing path as Bug #1), but the send path is
separate and did not error
, so the message was sent anyway — with a substituted
attachment.

Root cause (analysis)

Forwarded attachments are not copied at forward time. They are stored as a
reference and fetched lazily from IMAP at draft-save / send time:

// AttachmentService::handleForwardedAttachment (lines 376-401)
$mailbox = $this->mailManager->getMailbox($account->getUserId(), $attachment['mailboxId']);
$imapAttachment = $this->messageMapper->getAttachment(
    $client,
    $mailbox->getName(),
    (int)$attachment['uid'],   // source IMAP UID
    $attachment['id'],         // MIME part id
    $account->getUserId(),
);

The reference is {mailboxId, uid, part-id}. IMAP UIDs and MIME part-ids are not
content-pinned
: they identify a coordinate, not a file. When the sender/alias is
changed, the whole local message is re-serialized and these references are re-resolved
against IMAP. If the re-sent reference points at a different UID/part than the original
(stale frontend state, UIDVALIDITY change, part-id collision, or the same type-loss
corruption from Bug #1 mangling the attachment descriptor), getAttachment() returns
whatever file currently lives at that coordinate — which may be an unrelated
message's attachment. That file is then attached and sent.

Impact

  • Confidentiality: a file the sender never selected can be transmitted to a third
    party. If the substituted file belongs to a different message, this is an
    unintended disclosure.
  • Silent: the send path does not surface the draft-save error to the user.

Suggested fixes

  • Materialize forwarded attachments into local storage at forward time (as Bug fix nc branding #1's
    local uploads are), rather than resolving an IMAP coordinate at send time; or pin the
    reference to an immutable identifier (UIDVALIDITY + UID + part, validated before
    fetch).
  • Make handleAttachments() tolerant of a missing type for already-persisted
    attachments instead of throwing, and treat any unresolved reference as a hard send
    failure (never silently substitute).
  • Do not let a draft-save attachment error be bypassed by the send path.

Workaround

Do not change the sender/alias on a forward (or any message) that carries attachments.
On this instance the Mail app has been disabled pending a fix.

Mail app version

5.10.3

Nextcloud version

34.0.0.12

Mailserver or service

Exim4 and Dovecot

Operating system

Debian (Linux 6.12) / Docker (nextcloud:34-apache)

PHP engine version

Other

Nextcloud memory caching

Memcache

Web server

Apache (supported)

Database

MariaDB

Additional info

PHP 8.4.22, Apache
Accounts authenticated via IMAP/SMTP (app-password); LDAP backend for user accounts
MariaDB 11.8

Metadata

Metadata

Assignees

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions