Skip to content

8.5.12 upload-progress handler destroys the whole session when a stored object has a property named __PHP_Incomplete_Class_Name/_Serialized/__PHP_Incomplete_Enum_Case (regression) #24493

Open

Description

@VijhnAustin

Description

Since commit 31c0ea7d ("Avoid state corruption via bailout", CVE-2026-109618/109619), the pre-RINIT upload-progress session decoder runs in incomplete_classes_only mode and rejects any object property named __PHP_Incomplete_Class_Name, __PHP_Incomplete_Class_Serialized, or __PHP_Incomplete_Enum_Case (is_incomplete_class_reserved_member() in ext/standard/var_unserializer.re). The rejection is fail-closed via goto failure, which aborts the entire decode, so the whole session file is deleted ("Failed to decode session object. Session has been destroyed"). These are legal dynamic property names that PHP's own serialize() / session_encode() can emit. In 8.5.11 the same session round-trips unchanged.

Repro (default session.upload_progress.*; the session record is produced by session_encode(), not crafted bytes):

  1. A session stores an object with one of those property names — e.g. from a caller-controlled key:
<?php
session_start();
$o = new stdClass();
$o->{'__PHP_Incomplete_Class_Name'} = 'boo';   // e.g. $_SESSION['x'] = json_decode($userInput)
$_SESSION['x'] = $o;
  1. Any later multipart POST that carries the PHPSESSID field and the PHP_SESSION_UPLOAD_PROGRESS field triggers the pre-RINIT handler for that session.

Resulted in this output (8.5.12 / 8.4.27):

Warning: PHP Request Startup: Failed to decode session object. Session has been destroyed in Unknown on line 0

…and the session file is deleted.

But I expected this output instead (the 8.5.11 behavior): no warning, the session file kept byte-identical.

A/B over the three reserved names + a control property (a), records built by session_encode():

case 8.5.11 8.5.12
control (a) kept kept
__PHP_Incomplete_Class_Name kept (66 B) destroyed
__PHP_Incomplete_Class_Serialized kept (72 B) destroyed
__PHP_Incomplete_Enum_Case kept (65 B) destroyed

Mechanism: in process_nested_object_data(), when (*var_hash)->incomplete_classes_only is set and the key matches a reserved member, the code does zval_ptr_dtor(&key); goto failure; — aborting the whole record rather than dropping just that key. Rejecting the reserved names is necessary (they collide with the mode's own MAGIC_MEMBER* bookkeeping), but the blast radius could be narrowed: skip only the offending property (or mangle the key on write, like other internal-key handling) and keep the rest of the session, plus a regression test asserting the record survives.

Impact: forced session loss (logout / data loss) and log noise for apps that store object properties under caller-controlled names; otherwise self-inflicted. No memory-safety, confidentiality, or integrity impact — low severity, filed as a functional regression.

PHP Version

PHP 8.5.12 (cli) (built: Oct 20 2026 04:19:56) (NTS Visual C++ 2022 x64)
Copyright (c) The PHP Group
Built by The PHP Group
Zend Engine v4.5.12, Copyright (c) Zend Technologies

Operating System

Windows 11 (code path is OS-independent — ext/standard/var_unserializer.re)

Activity

  1. added a commit that references this issue on Oct 22, 2026
    a45ffe7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions