Skip to content

fix(security): restrict allowed_classes in unserialize() in cache handlers and Entity ArrayCast to prevent PHP Object Injection#10254

Open
XananasX7 wants to merge 2 commits into
codeigniter4:developfrom
XananasX7:security/cache-unserialize-allowed-classes
Open

fix(security): restrict allowed_classes in unserialize() in cache handlers and Entity ArrayCast to prevent PHP Object Injection#10254
XananasX7 wants to merge 2 commits into
codeigniter4:developfrom
XananasX7:security/cache-unserialize-allowed-classes

Conversation

@XananasX7
Copy link
Copy Markdown

@XananasX7 XananasX7 commented May 31, 2026

Description

PHP's unserialize() without allowed_classes will 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:

  1. ResponseCache::get() — the cached page data is always a plain array of [output, headers, status, reason]. Passing allowed_classes => false is safe here.
  2. RedisHandler and PredisHandler — the array branch of the match doesn't need to instantiate objects. I split it out so arrays get allowed_classes => false while the object branch stays unrestricted.
  3. ArrayCast::get() in the legacy Entity cast — the newer DataCaster/Cast/ArrayCast already 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:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

…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.
@mergeable
Copy link
Copy Markdown

mergeable Bot commented May 31, 2026

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
on the framework than you do. Please make it as painless for your contributions to be included as possible.

See https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md

Sincerely, the mergeable bot 🤖

@patel-vansh
Copy link
Copy Markdown
Contributor

This fix seems legit to me. The title of the PR should start with fix(security) or something. Also, I think you should add tests for this and also changelog entry too.

…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.
@XananasX7 XananasX7 changed the title [Security] Add allowed_classes to unserialize() in cache and Entity ArrayCast to prevent PHP Object Injection fix(security): restrict allowed_classes in unserialize() in cache handlers and Entity ArrayCast to prevent PHP Object Injection May 31, 2026
@XananasX7
Copy link
Copy Markdown
Author

Thanks @patel-vansh! I've addressed all your feedback:

  • Title renamed to fix(security): ... as suggested.
  • Tests added:
    • testGetResponseCacheRejectsObjectsInSerializedData() in ResponseCacheTest — verifies that a poisoned page-cache entry containing a serialised object is rejected and does not instantiate the class.
    • testCastArrayRejectsSerializedObjects() in EntityTest — verifies that an object serialised into a DB column via ArrayCast is not instantiated.
  • Changelog entry added to user_guide_src/source/changelogs/v4.7.4.rst for all three fixes (RedisHandler, PredisHandler, ResponseCache, ArrayCast).

Happy to add more test coverage or tweak anything if needed!

@samsonasik
Copy link
Copy Markdown
Member

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.

@XananasX7
Copy link
Copy Markdown
Author

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

@XananasX7
Copy link
Copy Markdown
Author

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:

  • ResponseCache::get() stores a plain array with scalar values (output, headers, status, reason). There's no legitimate object here, so allowed_classes => false is a safe, zero-behavior-change hardening.
  • RedisHandler / PredisHandler — I split the array/object match arm because arrays cached by CI don't contain objects. The object arm stays unrestricted since it legitimately needs to restore objects.
  • ArrayCast::get() in the legacy Entity cast — the newer DataCaster/Cast/ArrayCast already has this fix (added in https://github.com/codeigniter4/CodeIgniter4/blob/develop/system/DataCaster/Cast/ArrayCast.php). This is just applying the same pattern to the legacy equivalent.

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. allowed_classes => false cuts that off before anything gets instantiated.

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants