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
- Materialize results before storing: call $query->all() or fully iterate each() and persist the rows (or JSON).
- Never call unserialize() on user-controlled input — use JSON at trust boundaries.
- For legacy stored payloads, regenerate them with the fixed version instead of unserializing.
- 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
- Never call unserialize() on user-controlled input; use JSON at trust boundaries (CVE-2020-15148 class).
- Cache materialized arrays ($query->all()), never cursors from each()/batch().
- Pass ['allowed_classes' => false] (or an explicit allowlist) to every unserialize() call.
- After upgrading past Yii 2.0.38, regenerate any legacy serialized payloads instead of reviving them.
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
- Encryption requires the OpenSSL PHP extension
- {cipher} is not an allowed cipher
- Invalid parameters to hash_hkdf()
- Invalid parameters to hash_pbkdf2()
- Failed to generate HMAC with hash algorithm: {macHash}
AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17).
Data as JSON: /api/errors/f5fdfe57b602e0c3.
Report an issue: GitHub.