PHP版本低于8.1时readonly分析无意义;需静态扫描public/protected readonly属性,动态验证ReflectionProperty::setAccessible是否可绕过,检查__unserialize/__wakeup中是否校验readonly字段完整性。

在PHP框架审计中识别只读属性(readonly property)的潜在安全风险,需结合PHP 8.1+语法特性、框架运行时行为与反序列化上下文综合判断。只读属性一旦被绕过或误用,可能引发对象状态篡改、敏感数据泄露或反序列化链触发。
确认目标框架是否启用PHP 8.1+及strict_types
打开框架入口文件(如public/index.php或bootstrap/app.php),检查首行是否存在declare(strict_types=1);;同时确认composer.json中"php": "^8.1"或更高版本声明。若PHP版本低于8.1,readonly关键字将直接报错,无需继续分析该语法层面风险。
这一步必须先做——【PHP版本不达标时,所有readonly分析均无意义】。
静态扫描:定位所有声明为readonly的属性
使用grep -r "readonly \$" app/ --include="*.php"快速检索应用层代码;对框架核心(如Laravel的vendor/laravel/framework/src/Illuminate/)则需结合IDE符号跳转,重点查看Model、Request、DTO类中带readonly修饰的属性声明。
立即学习“PHP免费学习笔记(深入)”;
注意:仅扫描public readonly和protected readonly属性,private readonly因作用域限制,外部无法通过反射直接写入,暂不纳入高危范围。
动态验证:测试readonly属性是否可被反射强制修改
编写临时测试脚本test_readonly_bypass.php:
① 实例化一个含public readonly string $token;的类;
② 调用$ref = new ReflectionProperty($obj, 'token'); $ref->setAccessible(true); $ref->setValue($obj, 'hacked');;
③ 打印$obj->token并观察是否输出hacked。
若输出成功,说明该框架未禁用ReflectionProperty::setAccessible(true),且未在__wakeup()或反序列化钩子中校验readonly属性完整性——【此时readonly形同虚设,攻击者可通过POP链注入恶意值】。
检查反序列化上下文中的readonly属性处理逻辑
方法一:搜索__unserialize和__wakeup方法,确认是否对readonly属性执行了显式赋值。例如Laravel的Model类在__unserialize中会调用$this->syncOriginal();,但若开发者自定义反序列化逻辑且遗漏readonly字段校验,则存在覆盖风险。
方法二:在unserialize()调用点附近下断点,用Xdebug观察反序列化后对象属性内存状态,比对var_dump($obj)与debug_zval_dump($obj)中readonly字段的is_ref和refcount是否异常。
这一步操作起来很简单,直接把调试器挂到反序列化入口就行。



















