yiisoft/yii2 · error · BadMethodCallException

Cannot unserialize yii\db\BatchQueryResult

Error message

Cannot unserialize yii\db\BatchQueryResult

What it means

BatchQueryResult — the iterator returned by $query->each() and $query->batch() — deliberately blocks unserialization: __wakeup() throws BadMethodCallException, added in Yii 2.0.38 to blunt CVE-2020-15148, where unserializing attacker-crafted strings containing framework objects enabled remote code execution. Any unserialize() over a payload that embeds a serialized BatchQueryResult hits this guard immediately.

Source

Thrown at framework/db/BatchQueryResult.php:263

        if (!empty($this->_batch)) {
            $key = array_keys($this->_batch)[0];
            if (isset($this->_batch[$key]->db->driverName)) {
                return $this->_batch[$key]->db->driverName;
            }
        }

        return null;
    }

    /**
     * Unserialization is disabled to prevent remote code execution in case application
     * calls unserialize() on user input containing specially crafted string.
     * @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15148
     * @since 2.0.38
     */
    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
    }
}

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Materialize results before storing: call $query->all() or fully iterate each() and persist the rows (or JSON).
  2. Never call unserialize() on user-controlled input — use JSON at trust boundaries.
  3. For legacy stored payloads, regenerate them with the fixed version instead of unserializing.
  4. Catch BadMethodCallException around unserialize() of legacy blobs and treat them as invalid/expired.

Example fix

// before
$cursor = (new \yii\db\Query())->from('user')->each(100);
Yii::$app->cache->set('users', $cursor); // serializes the BatchQueryResult

// after
$users = (new \yii\db\Query())->from('user')->all();
Yii::$app->cache->set('users', $users);
Defensive patterns

Strategy: validation

Validate before calling

// Only unserialize payloads you produced and control — check the shape first
$data = \json_decode($raw, true);
if (\is_array($data) && !isset($data['__PHP_Incomplete_Class'])) {
    $restored = $data; // JSON path — no object wakeup involved
} else {
    $restored = null; // legacy serialized blob: regenerate instead of unserialize()
}

Try / catch

try {
    $obj = \unserialize($blob, ['allowed_classes' => false]);
} catch (\BadMethodCallException $e) {
    // blocked __wakeup (e.g. yii\db\BatchQueryResult guard) — treat as invalid
    \Yii::warning('Rejected serialized payload: ' . $e->getMessage(), 'security');
    $obj = false;
}

Prevention

When it happens

Trigger: Storing a live each()/batch() cursor in cache/session/queue payload and later restoring it with unserialize(); calling unserialize() on request input crafted as O:26:"yii\\db\\BatchQueryResult":...; generic object-caching layers that capture the iterator inside a bigger object graph; serialized blobs created before 2.0.38 being revived after upgrade.

Common situations: Caching ->each() results instead of ->all(); job queues serializing job properties holding query iterators; pentest scanners probing for CVE-2020-15148; upgrading Yii past 2.0.38 so old serialized payloads now fail to wake.

Related errors


AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17). Data as JSON: /api/errors/f5fdfe57b602e0c3. Report an issue: GitHub.