Skip to content

Clam 2947 improved email attachment extraction - #1720

Merged
val-ms merged 13 commits into
Cisco-Talos:mainfrom
val-ms:CLAM-2947-improved-email-attachment-extraction
Jun 22, 2026
Merged

Clam 2947 improved email attachment extraction#1720
val-ms merged 13 commits into
Cisco-Talos:mainfrom
val-ms:CLAM-2947-improved-email-attachment-extraction

Conversation

@val-ms

@val-ms val-ms commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR improves email attachment extraction for several MIME parsing edge
cases reported in CLAM-2947.

The changes add regression coverage for the reported .eml evasion patterns
and harden the mail parser around MIME boundary selection, header argument
parsing, partial-message reassembly, allocation failures, and malformed input.

Details

  • Handle MIME boundary parsing more consistently, including folded headers,
    multiple boundary-like parameters, semicolon-delimited parameters, and
    comments in header names.
  • Prefer the last applicable boundary argument for multipart parsing while
    ignoring boundary parameters from Content-Disposition.
  • Parse all relevant Content-Disposition parameters instead of only the first
    parameter.
  • Add bounds and heuristic checks for large header, MIME part, MIME argument,
    and message/partial inputs.
  • Rework message/partial reassembly to validate numeric arguments, check path
    lengths, avoid unsafe atoi behavior, and assemble parts with one checked
    directory pass.
  • Replace runtime assert() checks in text/message paths with explicit error
    handling so allocation failures preserve parser state.
  • Cast ctype() inputs through unsigned char in touched email parser paths.
  • Add encrypted .eml regression fixtures for the reported detection gaps.

Testing

  • git diff --check
  • make -j12
  • ctest -V -R libclamav

Credit: Artem Danilov at Positive Technologies

CLAM-2947

val-ms added 6 commits April 29, 2026 16:45
Email parsing could disagree with common mail clients when duplicate or
malformed MIME boundary declarations were present. That allowed attachments
behind the client-selected boundary to be skipped by the scanner.

Use the last Content-Type boundary value, keep invalid commented header names
from being normalized, parse quoted semicolons correctly, and ignore boundary
parameters on Content-Disposition. Add regression inputs for the reported
evasion cases.

Credit: Artem Danilov at Positive Technologies

CLAM-2947
Malformed MIME messages could drive unbounded work in nested part
headers and partial-message reassembly. A few parser helpers also
relied on assertions or undefined ctype input handling.

Apply the existing header and MIME count limits while parsing multipart
parts, validate partial-message numbers before reassembly, handle
allocation failures explicitly, and add regression fixtures for the
related MIME edge cases.

CLAM-2947
Email parsing checked every body line for MIME boundaries. The helpers
allocated and rescanned strings before rejecting ordinary content.

Skip non-boundary lines before allocation, scan semicolon-only lines
in one pass, and reuse the MIME argument buffer length while parsing
headers.

CLAM-2947
The email parser still had a few unsafe or expensive paths around
decoded line output, MIME argument handling, boundary matching, and
message/partial reassembly.

Bound decoded writes, preserve message list state on allocation
failures, parse all Content-Disposition parameters, and reassemble
message/partial inputs with a single checked directory pass.

CLAM-2947
Malformed quoted-printable input could consume the final output byte
without updating the remaining capacity. Content-Disposition parsing
also skipped the MIME argument cap, and text move or append failures
could drop or silently truncate message body data. Multipart part
construction could also proceed after allocation failures.

Tighten the fixed-buffer decode guard, account for malformed
quoted-printable output, and apply the existing MIME argument limit to
Content-Disposition parameters. Make text moves and appends report
allocation failure without losing the current chain. Treat multipart
part construction allocation failures as parse failures so partially
built parts are not scanned as complete.

CLAM-2947
The mbox parser had a couple of internal error paths that could leak
cl_error_t values through APIs that use local status or negative failure
returns. That confused enum types and could report failed partial-message
work as success.

Add an explicit mbox format-error status for MIME subtype parsing and
normalize partial-message helper failures to negative return values so
callers preserve the existing error checks.

CLAM-2947

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b107097b8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread libclamav/mbox.c Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens ClamAV’s email/MIME parsing to improve attachment extraction and close multiple evasion edge cases reported in CLAM-2947, and adds encrypted regression fixtures to ensure those cases stay covered.

Changes:

  • Adds new encrypted .eml regression fixtures and wires them into the unit test input set.
  • Improves MIME argument parsing (multipart boundary selection, Content-Disposition parameter parsing), adds bounds/heuristics, and reworks message/partial handling.
  • Replaces some assert() paths with explicit allocation-failure handling and fixes several ctype() callsites to cast via unsigned char.

Reviewed changes

Copilot reviewed 7 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
unit_tests/input/clamav_hdb_scanfiles/clam.mail-*.eml.xor Adds encrypted .eml regression fixtures for MIME parsing evasions.
unit_tests/input/CMakeLists.txt Registers the new encrypted .eml fixtures for test setup/decryption.
unit_tests/check_clamav.c Updates expected decrypted testfile count to include new fixtures.
libclamav/text.h Adds status-reporting variants for text/message merge helpers.
libclamav/text.c Implements status-reporting text merge/copy operations and replaces asserts with error handling.
libclamav/message.h Adds messageFindArgumentLast() to support “last boundary wins”.
libclamav/message.c Improves MIME argument parsing and allocation-failure robustness; adds messageFindArgumentLast().
libclamav/mbox.c Main parser hardening: boundary selection, MIME parameter iteration, heuristics, message/partial reassembly changes, and error handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread libclamav/mbox.c
Comment thread libclamav/mbox.c
Comment thread libclamav/mbox.c
Comment thread libclamav/mbox.c
MIME header lookups stopped stripping RFC822 comments from header
names before matching them against the RFC821 table. That regressed
malformed but accepted headers such as Content-Type(comment): and could
skip boundary or attachment metadata.

Route RFC821 header-name lookups through a helper that strips RFC822
comments, handle allocation failures in the MIME continuation path, and
tighten the comment-header fixtures so detection depends on the commented
header.

CLAM-2947
@val-ms
val-ms requested a review from Copilot April 30, 2026 12:36
@val-ms

val-ms commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 14 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

libclamav/text.c:261

  • textAddMessageWithStatus() doesn’t handle messageToText() returning NULL (e.g., OOM). In that case textMoveWithStatus() treats t == NULL as success, status remains 0, and the message body is silently dropped. Add an explicit NULL check after messageToText() and set *status = -1 (and return aText) when conversion fails.
        text *anotherText = messageToText(aMessage);

        if (aText) {
            int moveStatus = 0;
            text *newHead  = textMoveWithStatus(aText, anotherText, &moveStatus);

            if (moveStatus < 0) {
                textDestroy(anotherText);
                if (status)
                    *status = -1;
                return aText;
            }
            free(anotherText);
            return newHead;
        }
        return anotherText;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

textMoveWithStatus() empties the temporary text head on success.

The wrapper still needs textDestroy() for consistent cleanup.

Destroy the temporary wrapper after each move attempt.

If moving fails, preserve the existing accumulated text.

Thank-you to Jiasheng Jiang for identifying this issue.

@jhumlick jhumlick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a couple of issue:

libclamav/mbox.c:3144
boundaryEnd() no longer accepts trailing whitespace after closing MIME boundaries like --boundary-- . The old code trimmed trailing spaces before matching, so this can regress malformed-but-supported messages.

libclamav/mbox.c:1974 and libclamav/mbox.c:2159
The new multipart header byte/fold limits don’t count folded continuation lines consumed by next_is_folded_header(). A large folded MIME-part header can still grow fullline while bypassing the new counters.

Recent mbox parser hardening stopped accepting whitespace after closing
multipart boundaries and missed folded MIME-part lines consumed while
assembling a logical header. That could regress malformed but supported
messages and still allow oversized folded headers to grow in memory.

Accept only transport whitespace after --boundary-- markers, and count
each consumed folded continuation against the part header byte and fold
limits before appending it to the accumulated header line.

CLAM-2947

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 14 changed files in this pull request and generated 1 comment.

Comment thread libclamav/text.c Outdated
@jhumlick

jhumlick commented May 8, 2026

Copy link
Copy Markdown
Contributor

PARSE_HEADER_ALLOC_FAIL is still swallowed in the top-level header parsers. parseEmailFile() treats any parseEmailHeader(...) < 0 as “continue folding/ignore” at libclamav/mbox.c#L984 and #L1105; parseEmailHeaders() does the same at #L1330. Since this PR added a distinct allocation-failure return, these call sites should handle PARSE_HEADER_ALLOC_FAIL separately and abort/mark truncated instead of continuing with partially parsed MIME headers.

The non-default #ifndef SAVE_TO_DISC path still calls the old parseEmailHeaders(aMessage, table) signature at libclamav/mbox.c#L4865. If that path is kept buildable, this needs the new bool heuristicFound argument and handling.

Two review findings showed that top-level email header parsers
still treated parseEmailHeader() allocation failures like malformed
headers. That could continue parsing partially populated MIME metadata
instead of reporting the message as truncated.

Return NULL from textToFileblob() for NULL input, matching
textToBlob(). Propagate header allocation failures through the existing
truncated-message path, and update the non-default in-memory message
path to use the current parseEmailHeaders() signature.

CLAM-2947

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 14 changed files in this pull request and generated 3 comments.

Comment thread libclamav/mbox.c
Comment thread libclamav/text.c Outdated
Comment thread libclamav/mbox.c Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

We were unable to download your code in a timely manner.
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Review follow-up found mail parser paths that still relied on
split cleanup and stale ownership assumptions. That made future
fixes more likely to introduce double frees or missed cleanup when
error handling changed.

Convert cli_parse_mbox() to a single done: cleanup path and clear
ownership transfers between body and m. Also remove stale partial
files only when temporary files are not being kept, and make
textAddWithStatus() report copy failures without a caller-supplied
status pointer.

CLAM-2947
@val-ms
val-ms requested a review from Copilot May 18, 2026 21:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5b0db1345

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread libclamav/mbox.c
@val-ms

val-ms commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

Review follow-up pushed and the inline threads are resolved.

  • d5b0db1 reworks cli_parse_mbox() to use a single done: cleanup path, so the truncation path no longer frees body inline and then falls through to use or free it again.
  • d5b0db1 updates textAddWithStatus() to track copy failures even when status is NULL, returning NULL for status-less callers instead of silently truncating the chain.
  • d5b0db1 fixes stale message/partial cleanup so old partial files are removed only when temporary files are not being kept.
  • 89c0799 handles PARSE_HEADER_ALLOC_FAIL distinctly in the top-level header parsers and updates the non-default SAVE_TO_DISC path to the current parseEmailHeaders() signature.

Verification run locally: git diff --check, make -j12, and ctest -V -R "libclamav|clamscan".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 14 changed files in this pull request and generated no new comments.

The stale message/partial cleanup path unlinked candidate files
while the test descriptor was still open. That can fail on Windows
and cause reassembly to abort before scanning the reconstructed
message.

Close the descriptor before calling cli_unlink() for stale
partial files, and keep the shared cleanup close for non-stale files.

CLAM-2947

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 14 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 14 changed files in this pull request and generated no new comments.

@val-ms
val-ms requested a review from jhumlick May 19, 2026 18:19

@jhumlick jhumlick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Suppressed Copilot concern still looks valid. In textAddMessageWithStatus(), messageToText(aMessage) can return NULL, but the function does not set *status = -1. If aText already exists, textMoveWithStatus(aText, NULL, ...) returns success, so the decoded body is silently dropped while callers like parseEmailBody() see textStatus == 0. See libclamav/text.c at #L246 and callers at libclamav/mbox.c#L1817 / #L2512. This undercuts the PR’s allocation-failure hardening.

textAddMessageWithStatus() could treat a failed messageToText()
conversion as a successful append when existing text had already been
collected. That let mail parsing continue after silently dropping the
current decoded body.

Check the converted text before calling textMoveWithStatus(). Preserve
any existing accumulated text, but report failure through the status
output so callers can stop parsing safely.

CLAM-2947
@val-ms

val-ms commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

@jhumlick I pushed 3286694 to address the textAddMessageWithStatus() review finding. The function now checks messageToText() before calling textMoveWithStatus(); if conversion returns NULL, it preserves any accumulated text but sets *status = -1 so the mbox callers stop parsing safely instead of treating the append as successful.

Verification run locally: git diff --check, make -j12, and ctest -V -R "libclamav|clamscan".

@val-ms
val-ms requested a review from jhumlick June 4, 2026 15:10
@val-ms
val-ms merged commit f8c4138 into Cisco-Talos:main Jun 22, 2026
23 of 24 checks passed
@val-ms
val-ms deleted the CLAM-2947-improved-email-attachment-extraction branch June 22, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants