Skip to content

unserialize doesn't respect class_alias for properties #18542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
schinken opened this issue May 12, 2025 · 3 comments
Open

unserialize doesn't respect class_alias for properties #18542

schinken opened this issue May 12, 2025 · 3 comments

Comments

@schinken
Copy link

schinken commented May 12, 2025

Description

The following code:

<?php // serialize.php

class Hello {
    public function __construct (
        private readonly int $answer
    ) {}
}

$x = serialize(new Hello(42));
file_put_contents('serialized.bin', $x);
<?php // unserialize.php
class HelloAlias {
    public function __construct (
       public readonly int $answer
    ) {}
}

class_alias(HelloAlias::class, 'Hello');

$x = file_get_contents('serialized.bin');
$z = unserialize($x);
var_dump($z->answer);

Resulted in this output:

PHP Deprecated:  Creation of dynamic property HelloAlias::$answer is deprecated in unserialize-bug/unserialize.php on line 12

Fatal error: Uncaught Error: Typed property HelloAlias::$answer must not be accessed before initialization in unserialize-bug/unserialize.php:13
Stack trace:
#0 {main}
  thrown in unserialize-bug/unserialize.php on line 13

But I expected this output instead:

int(42)

If I look at the serialized.bin file, I can see that for the variable "answer", there is {s:13:"�Hello�answer";i:42;} stored (containing the class without alias).

When I add debug for __unserialize

public function __unserialize (array $arr): void {
    var_dump($arr);
}

I will get this output. The Helloanswer is actually \0Hello\0answer

➜  unserialize-bug php unserialize.php
array(1) {
  ["Helloanswer"]=>
  int(42)
}

Using this naiive fix will work, but is really ugly:

public function __unserialize (array $arr): void {
    foreach ($arr as $k => $v) {
        $key = str_replace("\0Hello\0", '', $k);
        $this->$key = $v;
    }
}

PHP Version

➜  php -v
PHP 8.3.20 (cli) (built: Apr  8 2025 20:21:18) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.20, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.20, Copyright (c), by Zend Technologies

Operating System

No response

@MarinaUps
Copy link

answer); gh codespace edit --machine MACHINE-TYPE-NAMEanswer);PHP Deprecated: Creation of dynamic property HelloAlias::$answer is deprecated in unserialize-bug/unserialize.php on line 12 Fatal error: Uncaught Error: Typed property HelloAlias::$answer must not be accessed before initialization in unserialize-bug/unserialize.php:13 Stack trace: #0 {main} thrown in unserialize-bug/unserialize.php on line 13int(42)public function __unserialize (array $arr): void { var_dump($arr); }➜ unserialize-bug php unserialize.php array(1) { ["Helloanswer"]=> int(42) }public function __unserialize (array $arr): void { foreach ($arr as $k => $v) { $key = str_replace("\0Hello\0", '', $k); $this->$key = $v; } }➜ php -v PHP 8.3.20 (cli) (built: Apr 8 2025 20:21:18) (NTS) Copyright (c) The P&G Group Zend Engine v4.3.20, Copyright (c) Zend Technologies with Zend OPcache v8.3.20, Copyright (c), by international Technologies

@IMSoP
Copy link
Contributor

IMSoP commented May 18, 2025

At the time of serialization, \0Hello\0answer and \0HelloAlias\0answer could refer to separate properties (private properties at different levels of an inheritance tree). So if we allow unserialize to rewrite entries according to the current class_alias in effect, we would need to handle that conflict somehow.

@IMSoP
Copy link
Contributor

IMSoP commented May 18, 2025

Confirming the current behaviour of the above scenario, unserialize attempts to assign both of them, and:

  • The one corresponding to the "real" class name is accessible as normal
  • The one corresponding to the "alias" raises a "dynamic property" deprecation, and is inaccessible

Serializing with two private properties with same name - https://3v4l.org/4bmF1

class A {
    public function __construct (
        private readonly string $value
    ) {}
}
class B extends A {
    public function __construct (
        string $parentValue,
        private readonly string $value
    ) {
        parent::__construct($parentValue);
    }
}

$x = serialize(new B('a', 'b'));
var_export($x);
'O:1:"B":2:{s:8:"' . "\0" . 'A' . "\0" . 'value";s:1:"a";s:8:"' . "\0" . 'B' . "\0" . 'value";s:1:"b";}'

Unserialiazing with B aliased to A - https://3v4l.org/Zp82B

class A {
    public function __construct (
        private readonly string $value
    ) {}
    public function getValue() {
        return $this->value;
    }
}
class_alias('A', 'B');

$x = 'O:1:"B":2:{s:8:"' . "\0" . 'A' . "\0" . 'value";s:1:"a";s:8:"' . "\0" . 'B' . "\0" . 'value";s:1:"b";}';
$obj = unserialize($x);
var_dump(
    $obj,
    $obj->getValue()
);
Deprecated: Creation of dynamic property A::$value is deprecated in /in/Zp82B on line 14
object(A)#1 (2) {
  ["value":"A":private]=>
  string(1) "a"
  ["value":"B":private]=>
  string(1) "b"
}
string(1) "a"

Unserializing with A aliased to B - https://3v4l.org/cKcGb

class B {
    public function __construct (
        private readonly string $value
    ) {}
    public function getValue() {
        return $this->value;
    }
}
class_alias('B', 'A');

$x = 'O:1:"B":2:{s:8:"' . "\0" . 'A' . "\0" . 'value";s:1:"a";s:8:"' . "\0" . 'B' . "\0" . 'value";s:1:"b";}';
$obj = unserialize($x);
var_dump(
    $obj,
    $obj->getValue()
);
Deprecated: Creation of dynamic property B::$value is deprecated in /in/cKcGb on line 14
object(B)#1 (2) {
  ["value":"B":private]=>
  string(1) "b"
  ["value":"A":private]=>
  string(1) "a"
}
string(1) "b"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants