|
| 1 | +--TEST-- |
| 2 | +unserialize() respects class_alias for private properties |
| 3 | +--FILE-- |
| 4 | +<?php |
| 5 | + |
| 6 | +// Test 1: Basic class_alias with private property |
| 7 | +class HelloAlias { |
| 8 | + public function __construct( |
| 9 | + private readonly int $answer = 0 |
| 10 | + ) {} |
| 11 | + |
| 12 | + public function getAnswer(): int { |
| 13 | + return $this->answer; |
| 14 | + } |
| 15 | +} |
| 16 | +class_alias(HelloAlias::class, 'Hello'); |
| 17 | + |
| 18 | +$serialized = 'O:5:"Hello":1:{s:13:"' . "\0Hello\0" . 'answer";i:42;}'; |
| 19 | +$obj = unserialize($serialized); |
| 20 | +var_dump($obj instanceof HelloAlias); |
| 21 | +var_dump($obj->getAnswer()); |
| 22 | + |
| 23 | +// Test 2: Protected property (should continue to work) |
| 24 | +class ProtoAlias { |
| 25 | + public function __construct( |
| 26 | + protected int $value = 0 |
| 27 | + ) {} |
| 28 | + public function getValue(): int { return $this->value; } |
| 29 | +} |
| 30 | +class_alias(ProtoAlias::class, 'Proto'); |
| 31 | + |
| 32 | +$serialized2 = 'O:5:"Proto":1:{s:8:"' . "\0*\0" . 'value";i:99;}'; |
| 33 | +$obj2 = unserialize($serialized2); |
| 34 | +var_dump($obj2 instanceof ProtoAlias); |
| 35 | +var_dump($obj2->getValue()); |
| 36 | + |
| 37 | +// Test 3: Public property (should continue to work) |
| 38 | +class PubAlias { |
| 39 | + public int $data = 0; |
| 40 | +} |
| 41 | +class_alias(PubAlias::class, 'Pub'); |
| 42 | + |
| 43 | +$serialized3 = 'O:3:"Pub":1:{s:4:"data";i:77;}'; |
| 44 | +$obj3 = unserialize($serialized3); |
| 45 | +var_dump($obj3 instanceof PubAlias); |
| 46 | +var_dump($obj3->data); |
| 47 | + |
| 48 | +// Test 4: Inheritance — child class with parent's private property via alias |
| 49 | +class ParentClass { |
| 50 | + public function __construct( |
| 51 | + private int $secret = 0 |
| 52 | + ) {} |
| 53 | + public function getSecret(): int { return $this->secret; } |
| 54 | +} |
| 55 | + |
| 56 | +class ChildClass extends ParentClass { |
| 57 | + public function __construct( |
| 58 | + private int $childProp = 0, |
| 59 | + int $secret = 0 |
| 60 | + ) { |
| 61 | + parent::__construct($secret); |
| 62 | + } |
| 63 | + public function getChildProp(): int { return $this->childProp; } |
| 64 | +} |
| 65 | +class_alias(ChildClass::class, 'Kid'); |
| 66 | + |
| 67 | +// Serialized with alias name for the child's private prop, canonical name for parent's |
| 68 | +$serialized4 = 'O:3:"Kid":2:{s:14:"' . "\0Kid\0" . 'childProp";i:10;s:19:"' . "\0ParentClass\0" . 'secret";i:20;}'; |
| 69 | +$obj4 = unserialize($serialized4); |
| 70 | +var_dump($obj4 instanceof ChildClass); |
| 71 | +var_dump($obj4->getChildProp()); |
| 72 | +var_dump($obj4->getSecret()); |
| 73 | + |
| 74 | +// Test 5: Multiple private properties with alias |
| 75 | +class MultiPropAlias { |
| 76 | + public function __construct( |
| 77 | + private int $x = 0, |
| 78 | + private string $y = '', |
| 79 | + private bool $z = false |
| 80 | + ) {} |
| 81 | + public function getX(): int { return $this->x; } |
| 82 | + public function getY(): string { return $this->y; } |
| 83 | + public function getZ(): bool { return $this->z; } |
| 84 | +} |
| 85 | +class_alias(MultiPropAlias::class, 'Multi'); |
| 86 | + |
| 87 | +$serialized5 = 'O:5:"Multi":3:{s:8:"' . "\0Multi\0" . 'x";i:1;s:8:"' . "\0Multi\0" . 'y";s:3:"abc";s:8:"' . "\0Multi\0" . 'z";b:1;}'; |
| 88 | +$obj5 = unserialize($serialized5); |
| 89 | +var_dump($obj5 instanceof MultiPropAlias); |
| 90 | +var_dump($obj5->getX()); |
| 91 | +var_dump($obj5->getY()); |
| 92 | +var_dump($obj5->getZ()); |
| 93 | + |
| 94 | +// Test 6: Canonical name still works (non-alias path, regression check) |
| 95 | +$serialized6 = 'O:14:"MultiPropAlias":3:{s:17:"' . "\0MultiPropAlias\0" . 'x";i:5;s:17:"' . "\0MultiPropAlias\0" . 'y";s:2:"hi";s:17:"' . "\0MultiPropAlias\0" . 'z";b:0;}'; |
| 96 | +$obj6 = unserialize($serialized6); |
| 97 | +var_dump($obj6 instanceof MultiPropAlias); |
| 98 | +var_dump($obj6->getX()); |
| 99 | +var_dump($obj6->getY()); |
| 100 | +var_dump($obj6->getZ()); |
| 101 | + |
| 102 | +// Test 7: Shadowed private properties — alias must not cross-contaminate |
| 103 | +// (Addresses concern from GH-18542: \0Alias\0prop and \0Parent\0prop refer |
| 104 | +// to different private properties at different inheritance levels) |
| 105 | +class Base { |
| 106 | + public function __construct( |
| 107 | + private string $prop = '' |
| 108 | + ) {} |
| 109 | + public function getBaseProp(): string { return $this->prop; } |
| 110 | +} |
| 111 | + |
| 112 | +class Child extends Base { |
| 113 | + public function __construct( |
| 114 | + private string $prop = '', |
| 115 | + string $baseProp = '' |
| 116 | + ) { |
| 117 | + parent::__construct($baseProp); |
| 118 | + } |
| 119 | + public function getChildProp(): string { return $this->prop; } |
| 120 | +} |
| 121 | +class_alias(Child::class, 'ChildAlias'); |
| 122 | + |
| 123 | +// Serialize with alias name for Child's prop, canonical for Base's prop. |
| 124 | +// These are TWO SEPARATE private properties that must not be confused. |
| 125 | +$serialized7 = 'O:10:"ChildAlias":2:{s:16:"' . "\0ChildAlias\0" . 'prop";s:5:"child";s:10:"' . "\0Base\0" . 'prop";s:4:"base";}'; |
| 126 | +$obj7 = unserialize($serialized7); |
| 127 | +var_dump($obj7 instanceof Child); |
| 128 | +var_dump($obj7->getChildProp()); |
| 129 | +var_dump($obj7->getBaseProp()); |
| 130 | + |
| 131 | +// Test 8: IMSoP's scenario from GH-18542 — alias and canonical name for |
| 132 | +// the SAME private property in a single class (no inheritance). |
| 133 | +// Both \0Bravo\0value and \0BravoAlias\0value refer to the same declared property. |
| 134 | +// The fix resolves both correctly; last one in serialized data wins. |
| 135 | +class Bravo { |
| 136 | + public function __construct( |
| 137 | + private string $value = '' |
| 138 | + ) {} |
| 139 | + public function getValue(): string { return $this->value; } |
| 140 | +} |
| 141 | +class_alias(Bravo::class, 'BravoAlias'); |
| 142 | + |
| 143 | +// Only alias-mangled name |
| 144 | +$serialized8 = 'O:10:"BravoAlias":1:{s:17:"' . "\0BravoAlias\0" . 'value";s:5:"alias";}'; |
| 145 | +$obj8 = unserialize($serialized8); |
| 146 | +var_dump($obj8 instanceof Bravo); |
| 147 | +var_dump($obj8->getValue()); |
| 148 | + |
| 149 | +// Only canonical name |
| 150 | +$serialized8b = 'O:10:"BravoAlias":1:{s:12:"' . "\0Bravo\0" . 'value";s:9:"canonical";}'; |
| 151 | +$obj8b = unserialize($serialized8b); |
| 152 | +var_dump($obj8b instanceof Bravo); |
| 153 | +var_dump($obj8b->getValue()); |
| 154 | + |
| 155 | +// Both alias AND canonical in same payload — last write wins, no dynamic property |
| 156 | +$serialized8c = 'O:10:"BravoAlias":2:{s:12:"' . "\0Bravo\0" . 'value";s:5:"first";s:17:"' . "\0BravoAlias\0" . 'value";s:4:"last";}'; |
| 157 | +$obj8c = unserialize($serialized8c); |
| 158 | +var_dump($obj8c instanceof Bravo); |
| 159 | +var_dump($obj8c->getValue()); |
| 160 | + |
| 161 | +echo "Done\n"; |
| 162 | +?> |
| 163 | +--EXPECT-- |
| 164 | +bool(true) |
| 165 | +int(42) |
| 166 | +bool(true) |
| 167 | +int(99) |
| 168 | +bool(true) |
| 169 | +int(77) |
| 170 | +bool(true) |
| 171 | +int(10) |
| 172 | +int(20) |
| 173 | +bool(true) |
| 174 | +int(1) |
| 175 | +string(3) "abc" |
| 176 | +bool(true) |
| 177 | +bool(true) |
| 178 | +int(5) |
| 179 | +string(2) "hi" |
| 180 | +bool(false) |
| 181 | +bool(true) |
| 182 | +string(5) "child" |
| 183 | +string(4) "base" |
| 184 | +bool(true) |
| 185 | +string(5) "alias" |
| 186 | +bool(true) |
| 187 | +string(9) "canonical" |
| 188 | +bool(true) |
| 189 | +string(4) "last" |
| 190 | +Done |
0 commit comments