Skip to content

Commit 2302753

Browse files
committed
Avoids code execution during cache deserialization
Improves cache security by extracting the serialized string from the cache payload instead of using `eval()` to deserialize it. This prevents potential code execution vulnerabilities. It also adds a helper function to the test class for exporting variables as strings.
1 parent 41cd0c9 commit 2302753

File tree

3 files changed

+80
-12
lines changed

3 files changed

+80
-12
lines changed

src/Cache/PsrCacheAdapter.php

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,46 @@ public function clear(?string $regex = null): void
9090
*/
9191
private function decodePayload(string $content): ?RegexNode
9292
{
93-
$code = ltrim($content);
93+
$serialized = $this->extractSerializedString($content);
94+
if (null === $serialized) {
95+
return null;
96+
}
9497

98+
$value = @unserialize($serialized, ['allowed_classes' => true]);
99+
100+
return $value instanceof RegexNode ? $value : null;
101+
}
102+
103+
/**
104+
* Extracts the serialized AST string from the generated payload without executing it.
105+
*/
106+
private function extractSerializedString(string $content): ?string
107+
{
108+
$code = ltrim($content);
95109
if (str_starts_with($code, '<?php')) {
96110
$code = substr($code, 5);
97111
}
98112

99-
try {
100-
$result = eval($code);
113+
$offset = stripos($code, 'unserialize(');
114+
if (false === $offset) {
115+
return null;
116+
}
101117

102-
return $result instanceof RegexNode ? $result : null;
103-
} catch (\Throwable) {
104-
// If anything goes wrong we just fall back to storing the raw payload.
118+
$argumentBlock = substr($code, $offset + \strlen('unserialize('));
119+
$commaPos = strpos($argumentBlock, ',');
120+
if (false === $commaPos) {
105121
return null;
106122
}
123+
124+
$argument = trim(substr($argumentBlock, 0, $commaPos));
125+
if ('' === $argument) {
126+
return null;
127+
}
128+
129+
if (\in_array($argument[0], ["'", '"'], true) && $argument[0] === substr($argument, -1)) {
130+
$argument = substr($argument, 1, -1);
131+
}
132+
133+
return stripcslashes($argument);
107134
}
108135
}

src/Cache/PsrSimpleCacheAdapter.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,46 @@ public function clear(?string $regex = null): void
8383
*/
8484
private function decodePayload(string $content): ?RegexNode
8585
{
86-
$code = ltrim($content);
86+
$serialized = $this->extractSerializedString($content);
87+
if (null === $serialized) {
88+
return null;
89+
}
8790

91+
$value = @unserialize($serialized, ['allowed_classes' => true]);
92+
93+
return $value instanceof RegexNode ? $value : null;
94+
}
95+
96+
/**
97+
* Extracts the serialized AST string from the generated payload without executing it.
98+
*/
99+
private function extractSerializedString(string $content): ?string
100+
{
101+
$code = ltrim($content);
88102
if (str_starts_with($code, '<?php')) {
89103
$code = substr($code, 5);
90104
}
91105

92-
try {
93-
$result = eval($code);
106+
$offset = stripos($code, 'unserialize(');
107+
if (false === $offset) {
108+
return null;
109+
}
94110

95-
return $result instanceof RegexNode ? $result : null;
96-
} catch (\Throwable) {
111+
$argumentBlock = substr($code, $offset + \strlen('unserialize('));
112+
$commaPos = strpos($argumentBlock, ',');
113+
if (false === $commaPos) {
97114
return null;
98115
}
116+
117+
$argument = trim(substr($argumentBlock, 0, $commaPos));
118+
if ('' === $argument) {
119+
return null;
120+
}
121+
122+
if (\in_array($argument[0], ["'", '"'], true) && $argument[0] === substr($argument, -1)) {
123+
$argument = substr($argument, 1, -1);
124+
}
125+
126+
return stripcslashes($argument);
99127
}
100128
}

tests/Unit/Cache/PsrCacheAdapterTest.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,15 @@ public function test_write_and_load_decoded_payload(): void
4848
$pool = new InMemoryPool();
4949
$cache = new PsrCacheAdapter($pool);
5050

51-
$payload = "<?php return new \\RegexParser\\Node\\RegexNode(new \\RegexParser\\Node\\LiteralNode('', 0, 0), '', '/', 0, 0);";
51+
$ast = new RegexNode(new LiteralNode('', 0, 0), '', '/', 0, 0);
52+
$serialized = serialize($ast);
53+
$payload = <<<PHP
54+
<?php
55+
56+
declare(strict_types=1);
57+
58+
return unserialize({$this->export($serialized)}, ['allowed_classes' => true]);
59+
PHP;
5260

5361
$key = $cache->generateKey('foo');
5462
$cache->write($key, $payload);
@@ -87,6 +95,11 @@ public function test_clear(): void
8795
$cache->clear();
8896
$this->assertNull($cache->load($key));
8997
}
98+
99+
private function export(string $value): string
100+
{
101+
return var_export($value, true);
102+
}
90103
}
91104

92105
final class InMemoryPool implements CacheItemPoolInterface

0 commit comments

Comments
 (0)