fix(security): restrict allowed_classes in unserialize() in cache handlers and Entity ArrayCast to prevent PHP Object Injection#10254
Conversation
…st to prevent PHP Object Injection Without an allowed_classes restriction, unserialize() is vulnerable to PHP Object Injection when an attacker can influence the serialized data (e.g. by writing to Redis, the file cache, or the database row that stores Entity data). Changes: - Cache/ResponseCache: stores only scalars (output string, headers array of strings, status int, reason string); restrict to allowed_classes => false - Cache/Handlers/RedisHandler: split 'array'/'object' match arms; arrays never contain objects so restrict 'array' case to allowed_classes => false - Cache/Handlers/PredisHandler: same split as RedisHandler - Entity/Cast/ArrayCast: the cast deserializes a database column value into a plain PHP array; restrict to allowed_classes => false (DataCaster/Cast/ArrayCast already has this restriction) No behavior change for legitimate data. Gadget chain exploitation via a compromised cache or database backend is prevented for the restricted call sites.
|
Hi there, XananasX7! 👋 Thank you for sending this PR! We expect the following in all Pull Requests (PRs).
Important We expect all code changes or bug-fixes to be accompanied by one or more tests added to our test suite to prove the code works. If pull requests do not comply with the above, they will likely be closed. Since we are a team of volunteers, we don't have any more time to work See https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md Sincerely, the mergeable bot 🤖 |
|
This fix seems legit to me. The title of the PR should start with |
…e fix - Add testGetResponseCacheRejectsObjectsInSerializedData() to verify that ResponseCache::get() does not instantiate injected objects from a poisoned cache backend (allowed_classes => false). - Add testCastArrayRejectsSerializedObjects() to verify that ArrayCast does not instantiate objects stored in database columns. - Add v4.7.4 changelog entries for all three security fixes.
|
Thanks @patel-vansh! I've addressed all your feedback:
Happy to add more test coverage or tweak anything if needed! |
|
I guess you're using AI generated for this PR, copy paste everything from GPT is hard to understand. We need contributor that actually understand and can explain the issue cleanly as a human, understand the flow and has effort to fix the notice feedback as a human. That will help us (maintainer) to review. Refine the PR as a human and follow PR template. Thank you for understanding. |
i use ai only for translate and quallity of preview |
|
Hey @samsonasik, fair feedback — appreciate you being direct about it. I rewrote the PR description to follow the template properly. To be upfront: I do use AI to help me find these patterns across codebases, but I understand what the code actually does and I stand behind this fix. Quick breakdown of the reasoning:
The risk: if someone can write to the Redis/file cache backend (or poison a DB column), they can craft a serialized payload that triggers a gadget chain. Tests cover the two main cases — ResponseCache rejecting a nested serialized object, and ArrayCast not instantiating an injected stdClass. Happy to add more specific cases if you think something's missing. Let me know if you want me to adjust anything. |
Description
PHP's
unserialize()withoutallowed_classeswill instantiate any class in the autoloader that's in the payload. A few places in CI4 call it on data that's expected to be plain arrays, not objects — so they're unnecessarily exposed to PHP Object Injection if an attacker can influence what gets stored (cache poisoning, SQL injection, etc).Three spots I found:
ResponseCache::get()— the cached page data is always a plain array of[output, headers, status, reason]. Passingallowed_classes => falseis safe here.RedisHandlerandPredisHandler— thearraybranch of thematchdoesn't need to instantiate objects. I split it out so arrays getallowed_classes => falsewhile the object branch stays unrestricted.ArrayCast::get()in the legacy Entity cast — the newerDataCaster/Cast/ArrayCastalready has this fix. This patch applies the same thing to the legacy one.Tests cover: ResponseCache rejecting a poisoned cache entry that contains a serialised object, and ArrayCast not instantiating a class from a poisoned DB column value.
Checklist: