false + plain-data check). * * Writers of these blobs (inventory, 18 Aug 2026): * - BxDolService::getSerializedService — used by cron transient jobs, background jobs, * live updates (comments/timeline), search-extended field values, ads form values_src. * - DB tables: sys_cron_jobs.service_call, background jobs.service_call, * sys_alerts_handlers.service_call, studio widget cnt_notices, live-update service_call. * - Studio/admin paths write alerts + widgets. A lower-privilege INSERT into those * columns is still "call any module method". This codec blocks object injection, * not an authorized service call. */ class BxDolServiceCallCodec { /** * JSON-encode a service-call array for new writes. */ public static function encode($mixedModule, $sMethod, $aParams = array(), $sClass = '') { $aService = array( 'module' => $mixedModule, 'method' => $sMethod, ); if (!empty($aParams)) $aService['params'] = $aParams; if (!empty($sClass)) $aService['class'] = $sClass; return json_encode($aService, JSON_UNESCAPED_UNICODE); } /** * True for a PHP-serialized array service call or a JSON object with module+method. */ public static function isEncoded($s) { return false !== self::decode($s); } /** * Decode a stored blob. Returns an array or false. * Never instantiates PHP objects from the payload. */ public static function decode($s) { if (!is_string($s) || $s === '') return false; $s = ltrim($s); if ($s === '') return false; if ($s[0] === '{') { $a = json_decode($s, true); return self::isServiceArray($a) ? $a : false; } // Historic PHP serialize of an array: a:N:{...} if (!preg_match('/^a:\d+:\{/', $s)) return false; $a = @unserialize($s, array('allowed_classes' => false)); return self::isServiceArray($a) ? $a : false; } /** * Same decode rules as BxBaseMenuSetAclLevel::getCode() AJAX profile_id. * Numeric string stays a string/number; otherwise json_decode. Never unserialize. */ public static function decodeAclProfileId($mixedProfileId) { if (!is_string($mixedProfileId) && !is_numeric($mixedProfileId)) return $mixedProfileId; $mixedProfileId = urldecode((string)$mixedProfileId); if (!is_numeric($mixedProfileId)) $mixedProfileId = json_decode($mixedProfileId, true); return $mixedProfileId; } protected static function isServiceArray($a) { if (!is_array($a) || !isset($a['module'], $a['method'])) return false; if (!is_scalar($a['module']) || !is_scalar($a['method'])) return false; return self::isPlainData($a); } protected static function isPlainData($v) { if (is_scalar($v) || $v === null) return true; if (!is_array($v)) return false; foreach ($v as $k => $item) { if (!is_int($k) && !is_string($k)) return false; if (!self::isPlainData($item)) return false; } return true; } }