[PECL-CVS] [pecl-mail-mailparse] master: Limit MIME nesting depth to prevent unbounded recursion
[email protected] (Ilia Alshanetsky via Remi Collet) Thu, 18 Jun 2026 06:36:22 +0000
| Newsgroups | php.pecl.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: Ilia Alshanetsky (iliaal)
Committer: Remi Collet (remicollet)
Date: 2026-06-18T08:31:20+02:00
Commit: https://github.com/php/pecl-mail-mailparse/commit/196b2900ae6d03e5525d67d75114d4cba041c9c5
Raw diff: https://github.com/php/pecl-mail-mailparse/commit/196b2900ae6d03e5525d67d75114d4cba041c9c5.diff
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.
Changed paths:
A tests/mime_nesting_depth.phpt
M php_mailparse_mime.c
Diff:
diff --git a/php_mailparse_mime.c b/php_mailparse_mime.c
index c6a17ab..28d0dd7 100644
--- a/php_mailparse_mime.c
+++ b/php_mailparse_mime.c
@@ -521,8 +521,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;
@@ -617,6 +627,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);
@@ -714,6 +728,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;
@@ -722,6 +740,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--
+<?php if (!extension_loaded("mailparse")) print "skip"; ?>
+--FILE--
+<?php
+$msg = str_repeat("Content-Type: message/rfc822\n\n", 1000);
+$m = mailparse_msg_create();
+var_dump(mailparse_msg_parse($m, $msg));
+/* walking the (capped) tree must not crash */
+mailparse_msg_get_structure($m);
+echo "done\n";
+mailparse_msg_free($m);
+?>
+--EXPECTF--
+Warning: mailparse_msg_parse(): MIME message too deeply nested in %s on line %d
+bool(false)
+done