Skip to content

Commit a5ac1be

Browse files
committed
Refactor the PHP session reader to avoid warnings from unserializing data on PHP 8.3
1 parent 6e770ec commit a5ac1be

File tree

1 file changed

+91
-10
lines changed

1 file changed

+91
-10
lines changed

src/Session/Reader/PhpReader.php

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,110 @@ final class PhpReader implements Reader
1515

1616
/**
1717
* @throws InvalidSession if the session data cannot be deserialized
18-
*
19-
* @see https://www.php.net/manual/en/function.session-decode.php#108037
2018
*/
2119
public function read(string $data): array
2220
{
2321
$deserialized = [];
2422
$offset = 0;
2523

2624
while ($offset < \strlen($data)) {
27-
if (!str_contains(substr($data, $offset), self::DELIMITER)) {
25+
$currentPos = strpos($data, self::DELIMITER, $offset);
26+
27+
if (false === $currentPos) {
2828
throw new InvalidSession($data, 'Cannot deserialize session data.');
2929
}
3030

31-
$pos = strpos($data, self::DELIMITER, $offset);
32-
$num = $pos - $offset;
33-
$variable = substr($data, $offset, $num);
34-
$offset += $num + 1;
35-
$deserializedSection = unserialize(substr($data, $offset));
31+
$name = substr($data, $offset, $currentPos - $offset);
32+
$offset = $currentPos + 1;
33+
34+
// Find the position for the end of the serialized data so we can correctly chop the next variable if need be
35+
$serializedLength = $this->getSerializedSegmentLength(substr($data, $offset));
36+
37+
if (false === $serializedLength) {
38+
throw new InvalidSession($data, 'Cannot deserialize session data.');
39+
}
40+
41+
$rawData = substr($data, $offset, $serializedLength);
42+
43+
$value = unserialize($rawData);
3644

37-
$deserialized[$variable] = $deserializedSection;
38-
$offset += \strlen(serialize($deserializedSection));
45+
$deserialized[$name] = $value;
46+
47+
$offset += $serializedLength;
3948
}
4049

4150
return $deserialized;
4251
}
52+
53+
private function getSerializedSegmentLength(string $data): int|false
54+
{
55+
// No serialized value can have a length of less than 4 characters
56+
if (\strlen($data) < 4) {
57+
return false;
58+
}
59+
60+
// The data type will be in position 0
61+
switch ($data[0]) {
62+
// Null value
63+
case 'N':
64+
return 2;
65+
66+
// Boolean value
67+
case 'b':
68+
return 4;
69+
70+
// Integer or floating point value
71+
case 'i':
72+
case 'd':
73+
$end = strpos($data, ';');
74+
75+
return false === $end ? false : $end + 1;
76+
77+
// String value
78+
case 's':
79+
if (!preg_match('/^s:\d+:"/', $data, $matches)) {
80+
return false;
81+
}
82+
83+
// Add characters for the closing quote and semicolon
84+
return \strlen($matches[0]) + (int) substr($matches[0], 2, -2) + 2;
85+
86+
// Array value
87+
case 'a':
88+
if (!preg_match('/^a:\d+:\{/', $data, $matches)) {
89+
return false;
90+
}
91+
92+
$start = \strlen($matches[0]);
93+
$count = (int) substr($matches[0], 2, -2);
94+
$offset = $start;
95+
$length = \strlen($data);
96+
97+
// Double the count to account for each element having a key and value
98+
for ($i = 0; $i < $count * 2; ++$i) {
99+
$segmentLength = $this->getSerializedSegmentLength(substr($data, $offset));
100+
101+
if (false === $segmentLength) {
102+
return false;
103+
}
104+
105+
$offset += $segmentLength;
106+
107+
if ($offset >= $length) {
108+
return false;
109+
}
110+
}
111+
112+
if ('}' !== $data[$offset]) {
113+
return false;
114+
}
115+
116+
// Add characters for the closing brace
117+
return $offset + 1;
118+
119+
// Unsupported value
120+
default:
121+
return false;
122+
}
123+
}
43124
}

0 commit comments

Comments
 (0)