Skip to content
/ mime Public

Commit 7186d94

Browse files
[String][Mime] Reject objects in typed-string properties during __unserialize
1 parent b198dd6 commit 7186d94

4 files changed

Lines changed: 114 additions & 0 deletions

File tree

Part/SMimePart.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ public function __serialize(): array
119119

120120
public function __unserialize(array $data): void
121121
{
122+
foreach (['body', 'type', 'subtype'] as $prop) {
123+
if (($data[$prop] ?? $data["\0".self::class."\0".$prop] ?? $data["\0*\0".$prop] ?? null) instanceof \Stringable) {
124+
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
125+
}
126+
}
127+
122128
if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
123129
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
124130
}

Part/TextPart.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ public function __serialize(): array
276276

277277
public function __unserialize(array $data): void
278278
{
279+
foreach (['charset', 'subtype', 'disposition', 'name', 'encoding'] as $prop) {
280+
if (($data[$prop] ?? $data["\0".self::class."\0".$prop] ?? $data["\0*\0".$prop] ?? null) instanceof \Stringable) {
281+
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
282+
}
283+
}
284+
279285
if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
280286
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
281287
}

Tests/Part/SMimePartTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\Tests\Part;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Mime\Part\SMimePart;
17+
18+
class SMimePartTestToStringGadget
19+
{
20+
public static bool $fired = false;
21+
22+
public function __toString(): string
23+
{
24+
self::$fired = true;
25+
26+
return '';
27+
}
28+
}
29+
30+
class SMimePartTest extends TestCase
31+
{
32+
#[DataProvider('provideTrampolineKeys')]
33+
public function testUnserializeRejectsObjectInTypedStringProperty(string $key)
34+
{
35+
$template = (new SMimePart('body content', 'application', 'pkcs7-mime', []))->__serialize();
36+
$template[$key] = new SMimePartTestToStringGadget();
37+
$payload = \sprintf('O:%d:"%s":%d:{', \strlen(SMimePart::class), SMimePart::class, \count($template));
38+
foreach ($template as $k => $v) {
39+
$payload .= serialize($k).serialize($v);
40+
}
41+
$payload .= '}';
42+
SMimePartTestToStringGadget::$fired = false;
43+
44+
try {
45+
unserialize($payload);
46+
$this->fail('Expected BadMethodCallException.');
47+
} catch (\BadMethodCallException $e) {
48+
}
49+
50+
$this->assertFalse(SMimePartTestToStringGadget::$fired, '__toString gadget must not fire during unserialize');
51+
}
52+
53+
public static function provideTrampolineKeys(): iterable
54+
{
55+
yield ['body'];
56+
yield ['type'];
57+
yield ['subtype'];
58+
}
59+
}

Tests/Part/TextPartTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Mime\Tests\Part;
1313

14+
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\TestCase;
1516
use Symfony\Component\Mime\Encoder\ContentEncoderInterface;
1617
use Symfony\Component\Mime\Exception\InvalidArgumentException;
@@ -21,8 +22,50 @@
2122
use Symfony\Component\Mime\Part\File;
2223
use Symfony\Component\Mime\Part\TextPart;
2324

25+
class TextPartTestToStringGadget
26+
{
27+
public static bool $fired = false;
28+
29+
public function __toString(): string
30+
{
31+
self::$fired = true;
32+
33+
return '';
34+
}
35+
}
36+
2437
class TextPartTest extends TestCase
2538
{
39+
#[DataProvider('provideTrampolineKeys')]
40+
public function testUnserializeRejectsObjectInTypedStringProperty(string $key)
41+
{
42+
$template = (new TextPart('body content'))->__serialize();
43+
$template[$key] = new TextPartTestToStringGadget();
44+
$payload = \sprintf('O:%d:"%s":%d:{', \strlen(TextPart::class), TextPart::class, \count($template));
45+
foreach ($template as $k => $v) {
46+
$payload .= serialize($k).serialize($v);
47+
}
48+
$payload .= '}';
49+
TextPartTestToStringGadget::$fired = false;
50+
51+
try {
52+
unserialize($payload);
53+
$this->fail('Expected BadMethodCallException.');
54+
} catch (\BadMethodCallException $e) {
55+
}
56+
57+
$this->assertFalse(TextPartTestToStringGadget::$fired, '__toString gadget must not fire during unserialize');
58+
}
59+
60+
public static function provideTrampolineKeys(): iterable
61+
{
62+
yield ['charset'];
63+
yield ['subtype'];
64+
yield ['disposition'];
65+
yield ['name'];
66+
yield ['encoding'];
67+
}
68+
2669
public function testConstructor()
2770
{
2871
$p = new TextPart('content');

0 commit comments

Comments
 (0)