From 9ed0a8a2b35125c3934ccad869f49922f54cfbad Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 7 Jun 2026 10:16:47 -0400 Subject: [PATCH] Limit MIME nesting depth to prevent unbounded recursion A message that nests one child part per level (e.g. repeated "Content-Type: message/rfc822" parts, or singly-nested multipart containers) was bounded only by MAXPARTS, which counts children per part and never trips for a one-child-per-level chain. Parse depth was therefore attacker-controlled and unbounded. php_mimepart_enum_parts() / enum_parts_recurse() walk the tree recursively, so a deep enough message overflowed the C stack (SIGSEGV) on mailparse_msg_get_structure(), mailparse_msg_get_part() and the extract paths. The descent in php_mimepart_process_line() also made parsing such a message quadratic. Enforce the existing (previously unused) MAXLEVELS cap in alloc_new_child_part(): refuse to create a child past the limit and fail the parse, matching the existing MAXPARTS handling. --- php_mailparse_mime.c | 24 +++++++++++++++++++++++- tests/mime_nesting_depth.phpt | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/mime_nesting_depth.phpt diff --git a/php_mailparse_mime.c b/php_mailparse_mime.c index e71bfec..789f345 100644 --- a/php_mailparse_mime.c +++ b/php_mailparse_mime.c @@ -523,8 +523,18 @@ static int php_mimepart_process_header(php_mimepart *part) static php_mimepart *alloc_new_child_part(php_mimepart *parentpart, size_t startpos, int inherit) { - php_mimepart *child = php_mimepart_alloc(); + php_mimepart *child; + php_mimepart *ancestor; zval child_z; + int depth = 0; + + for (ancestor = parentpart; ancestor != NULL; ancestor = ancestor->parent) { + if (++depth >= MAXLEVELS) { + return NULL; + } + } + + child = php_mimepart_alloc(); parentpart->parsedata.lastpart = child; child->parent = parentpart; @@ -619,6 +629,10 @@ static int php_mimepart_process_line(php_mimepart *workpart) } newpart = alloc_new_child_part(workpart, workpart->endpos + origcount, 1); + if (newpart == NULL) { + php_error_docref(NULL, E_WARNING, "MIME message too deeply nested"); + return FAILURE; + } php_mimepart_update_positions(workpart, workpart->endpos + origcount, workpart->endpos + linelen, 1); if (workpart->mime_version) { newpart->mime_version = estrdup(workpart->mime_version); @@ -716,6 +730,10 @@ static int php_mimepart_process_line(php_mimepart *workpart) if (CONTENT_TYPE_IS(workpart, "message/rfc822")) { workpart = alloc_new_child_part(workpart, workpart->bodystart, 0); + if (workpart == NULL) { + php_error_docref(NULL, E_WARNING, "MIME message too deeply nested"); + return FAILURE; + } workpart->parsedata.in_header = 1; return SUCCESS; @@ -724,6 +742,10 @@ static int php_mimepart_process_line(php_mimepart *workpart) /* create a section for the preamble that precedes the first boundary */ if (workpart->boundary) { workpart = alloc_new_child_part(workpart, workpart->bodystart, 1); + if (workpart == NULL) { + php_error_docref(NULL, E_WARNING, "MIME message too deeply nested"); + return FAILURE; + } workpart->parsedata.in_header = 0; workpart->parsedata.is_dummy = 1; return SUCCESS; diff --git a/tests/mime_nesting_depth.phpt b/tests/mime_nesting_depth.phpt new file mode 100644 index 0000000..e645fe5 --- /dev/null +++ b/tests/mime_nesting_depth.phpt @@ -0,0 +1,18 @@ +--TEST-- +Deeply nested MIME parts are rejected instead of overflowing the stack +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Warning: mailparse_msg_parse(): MIME message too deeply nested in %s on line %d +bool(false) +done