Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f3cadf1

Browse filesBrowse files
[Messenger] Sanitize binary strings in FlattenException trace args before normalizing
When an exception is thrown over a call frame whose argument contains non-UTF-8 bytes (e.g. a PDF body passed to a handler), the FlattenException captures the raw byte sequence in its trace. Encoding the resulting ErrorDetailsStamp through the JSON serializer then fails with NotEncodableValueException("Malformed UTF-8 characters, possibly incorrectly encoded"), so the message never reaches the failure transport for retry/inspection. Replace any non-UTF-8 string argument (recursively, including arrays of args) with a '(binary string)' placeholder during normalize(). The denormalizer is unchanged: the round trip is intentionally lossy for binary blobs that are only meaningful for diagnostics anyway. Direction was endorsed by @nicolas-grekas in #57535: the cleanup belongs in FlattenExceptionNormalizer, not in FlattenException itself, because FlattenException makes no UTF-8 promise.
1 parent d3c1e02 commit f3cadf1
Copy full SHA for f3cadf1

2 files changed

+76-1Lines changed: 76 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Symfony/Component/Messenger/Tests/Transport/Serialization/Normalizer/FlattenExceptionNormalizerTest.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Messenger/Tests/Transport/Serialization/Normalizer/FlattenExceptionNormalizerTest.php
+47Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,53 @@ public static function provideFlattenException(): array
6969
];
7070
}
7171

72+
public function testNormalizeReplacesBinaryStringTraceArgsWithJsonSafePlaceholder()
73+
{
74+
$exception = FlattenException::createFromThrowable(new \RuntimeException('boom'));
75+
76+
$trace = $exception->getTrace();
77+
$trace[] = [
78+
'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '',
79+
'function' => 'fromPdf', 'file' => 'foo.php', 'line' => 1,
80+
'args' => [
81+
['string', 'valid utf8'],
82+
['string', "\x80\x81\xfe"],
83+
['array', [
84+
['string', "\xff\xff"],
85+
['integer', 42],
86+
]],
87+
],
88+
];
89+
$traceProperty = new \ReflectionProperty(FlattenException::class, 'trace');
90+
$traceProperty->setValue($exception, $trace);
91+
92+
$normalized = $this->normalizer->normalize($exception, null, $this->getMessengerContext());
93+
94+
$args = $normalized['trace'][\count($normalized['trace']) - 1]['args'];
95+
$this->assertSame(['string', 'valid utf8'], $args[0]);
96+
$this->assertSame(['string', '(binary string, 3 bytes)'], $args[1]);
97+
$this->assertSame('array', $args[2][0]);
98+
$this->assertSame(['string', '(binary string, 2 bytes)'], $args[2][1][0]);
99+
$this->assertSame(['integer', 42], $args[2][1][1]);
100+
101+
$this->assertNotFalse(json_encode($normalized, \JSON_THROW_ON_ERROR));
102+
}
103+
104+
public function testNormalizeKeepsTraceAsStringJsonEncodableEvenWithBinaryArgs()
105+
{
106+
$exception = null;
107+
try {
108+
(function (string $pdf) { throw new \RuntimeException('boom'); })("\x80\x81\xfe\xff\xc3\x28");
109+
} catch (\Throwable $e) {
110+
$exception = FlattenException::createFromThrowable($e);
111+
}
112+
113+
$normalized = $this->normalizer->normalize($exception, null, $this->getMessengerContext());
114+
115+
$this->assertSame(1, preg_match('//u', $normalized['trace_as_string']), 'trace_as_string must be valid UTF-8 even when binary bytes are passed as call args (PHP escapes them as \\xNN)');
116+
$this->assertNotFalse(json_encode($normalized, \JSON_THROW_ON_ERROR));
117+
}
118+
72119
public function testSupportsDenormalization()
73120
{
74121
$this->assertFalse($this->normalizer->supportsDenormalization(null, FlattenException::class));
Collapse file

‎src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php
+29-1Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
3838
'previous' => null === $object->getPrevious() ? null : $this->normalize($object->getPrevious(), $format, $context),
3939
'status' => $object->getStatusCode(),
4040
'status_text' => $object->getStatusText(),
41-
'trace' => $object->getTrace(),
41+
'trace' => self::sanitizeTrace($object->getTrace()),
4242
'trace_as_string' => $object->getTraceAsString(),
4343
];
4444

@@ -87,4 +87,32 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form
8787
{
8888
return FlattenException::class === $type && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false);
8989
}
90+
91+
private static function sanitizeTrace(array $trace): array
92+
{
93+
foreach ($trace as &$frame) {
94+
if (isset($frame['args']) && \is_array($frame['args'])) {
95+
$frame['args'] = self::sanitizeArgs($frame['args']);
96+
}
97+
}
98+
99+
return $trace;
100+
}
101+
102+
private static function sanitizeArgs(array $args): array
103+
{
104+
foreach ($args as &$arg) {
105+
if (!\is_array($arg) || 2 !== \count($arg)) {
106+
continue;
107+
}
108+
[$type, $value] = $arg;
109+
if ('string' === $type && \is_string($value) && !preg_match('//u', $value)) {
110+
$arg[1] = \sprintf('(binary string, %d bytes)', \strlen($value));
111+
} elseif ('array' === $type && \is_array($value)) {
112+
$arg[1] = self::sanitizeArgs($value);
113+
}
114+
}
115+
116+
return $args;
117+
}
90118
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.