[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