Summary
The Live Helper Chat REST API chat update endpoint allows a REST user with lhchat/use to update a chat in a department they cannot read. The endpoint accepts arbitrary chat object fields, so the user can change the chat hash and status and then access or tamper with the chat through visitor/widget paths. The same write primitive can set operation_admin, which is later emitted as operator-side JavaScript.
Details
PUT /restapi/chat/{chat_id} is documented as requiring lhchat/use and an assigned department in write mode. In lhc_web/modules/lhrestapi/chat.php, the PUT branch validates the REST request, checks only lhchat/use, fetches the target chat by path ID, calls erLhcoreClassRestAPIHandler::hasAccessToWrite($chat), and then assigns every request-body attribute except id before calling $chat->saveThis().
The authorization check used by this branch is weaker than the read check. hasAccessToRead() verifies the API user's all_departments flag, assigned departments, ownership, and chat status rules. hasAccessToWrite() only checks whether the chat department is in erLhcoreClassUserDep::getUserReadDepartments(). That helper returns departments explicitly marked read-only for the user, not every department outside the user's scope. A user who is not assigned to the target department can therefore fail hasAccessToRead($chat) but still pass hasAccessToWrite($chat) if the target department is not explicitly present as read-only.
The chat model persists security-sensitive fields that are accepted by this mass assignment path. erLhcoreClassModelChat::getState() and the persistent mapping include hash, status, dep_id, user_id, and operation_admin. Setting hash and status is enough to make visitor/widget endpoints that trust chat_id + hash accept the attacker-chosen hash. Setting operation_admin stages JavaScript for operator polling: syncadmin.php returns oad=1, lh.js appends a script tag for chat/loadoperatorjs/(type)/chat/(id)/<chat_id>, and loadoperatorjs.php echoes the stored operation_admin value as text/javascript.
I did not identify a server-side RCE path from this primitive. The confirmed code path is cross-department chat modification/read access and operator-side same-origin JavaScript when an operator/admin browser processes the affected chat.
PoC
- Set up Live Helper Chat 4.84v with two departments. Create a REST API user with
lhchat/use, all_departments = 0, and assignment only to department A. Create or choose a target chat in department B.
- Confirm the intended boundary:
hasAccessToRead($targetChat) should return false for this API user because department B is not in the user's assigned departments.
- Send a PUT request with the normal REST API authentication for that user:
PUT /<siteaccess>/restapi/chat/<target_chat_id> HTTP/1.1
Content-Type: application/json
{
"hash": "attacker-controlled-hash",
"status": 1,
"operation_admin": "alert(document.domain)"
}
- Expected state transition from the audited code path: the
lh_chat row for <target_chat_id> now has the attacker-supplied hash, status, and operation_admin values even though the API user cannot read department B chats.
- Use the attacker-chosen hash against a visitor/widget chat endpoint that checks
chat_id + hash, such as the chat widget or message sync path, and observe that the target chat is accepted under the new hash.
- When an operator/admin browser polls or opens the affected chat,
syncadmin.php marks oad=1; the frontend loads chat/loadoperatorjs/(type)/chat/(id)/<target_chat_id>, and loadoperatorjs.php returns the stored operation_admin value as JavaScript.
Impact
A low-privileged REST API user can modify and access chats outside their assigned departments. This breaks the department isolation model and exposes chat contents and chat state to users who should not be able to read them. The operation_admin field also turns the same primitive into stored operator-side JavaScript execution in the Live Helper Chat origin. Follow-on actions through that JavaScript depend on the privileges of the operator/admin session that processes the affected chat.
Summary
The Live Helper Chat REST API chat update endpoint allows a REST user with
lhchat/useto update a chat in a department they cannot read. The endpoint accepts arbitrary chat object fields, so the user can change the chathashandstatusand then access or tamper with the chat through visitor/widget paths. The same write primitive can setoperation_admin, which is later emitted as operator-side JavaScript.Details
PUT /restapi/chat/{chat_id}is documented as requiringlhchat/useand an assigned department in write mode. Inlhc_web/modules/lhrestapi/chat.php, the PUT branch validates the REST request, checks onlylhchat/use, fetches the target chat by path ID, callserLhcoreClassRestAPIHandler::hasAccessToWrite($chat), and then assigns every request-body attribute exceptidbefore calling$chat->saveThis().The authorization check used by this branch is weaker than the read check.
hasAccessToRead()verifies the API user'sall_departmentsflag, assigned departments, ownership, and chat status rules.hasAccessToWrite()only checks whether the chat department is inerLhcoreClassUserDep::getUserReadDepartments(). That helper returns departments explicitly marked read-only for the user, not every department outside the user's scope. A user who is not assigned to the target department can therefore failhasAccessToRead($chat)but still passhasAccessToWrite($chat)if the target department is not explicitly present as read-only.The chat model persists security-sensitive fields that are accepted by this mass assignment path.
erLhcoreClassModelChat::getState()and the persistent mapping includehash,status,dep_id,user_id, andoperation_admin. Settinghashandstatusis enough to make visitor/widget endpoints that trustchat_id + hashaccept the attacker-chosen hash. Settingoperation_adminstages JavaScript for operator polling:syncadmin.phpreturnsoad=1,lh.jsappends a script tag forchat/loadoperatorjs/(type)/chat/(id)/<chat_id>, andloadoperatorjs.phpechoes the storedoperation_adminvalue astext/javascript.I did not identify a server-side RCE path from this primitive. The confirmed code path is cross-department chat modification/read access and operator-side same-origin JavaScript when an operator/admin browser processes the affected chat.
PoC
lhchat/use,all_departments = 0, and assignment only to department A. Create or choose a target chat in department B.hasAccessToRead($targetChat)should return false for this API user because department B is not in the user's assigned departments.lh_chatrow for<target_chat_id>now has the attacker-suppliedhash,status, andoperation_adminvalues even though the API user cannot read department B chats.chat_id + hash, such as the chat widget or message sync path, and observe that the target chat is accepted under the new hash.syncadmin.phpmarksoad=1; the frontend loadschat/loadoperatorjs/(type)/chat/(id)/<target_chat_id>, andloadoperatorjs.phpreturns the storedoperation_adminvalue as JavaScript.Impact
A low-privileged REST API user can modify and access chats outside their assigned departments. This breaks the department isolation model and exposes chat contents and chat state to users who should not be able to read them. The
operation_adminfield also turns the same primitive into stored operator-side JavaScript execution in the Live Helper Chat origin. Follow-on actions through that JavaScript depend on the privileges of the operator/admin session that processes the affected chat.