Skip to content

Commit 7dde925

Browse files
Merge pull request #7410 from christianbeeznest/fixes-updates228
Social: Fix IDOR authorization in SocialController legal/privacy endpoints
2 parents 879bb4a + 47b287c commit 7dde925

3 files changed

Lines changed: 134 additions & 79 deletions

File tree

src/CoreBundle/Controller/SocialController.php

Lines changed: 117 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
2727
use Chamilo\CoreBundle\Repository\Node\UserRepository;
2828
use Chamilo\CoreBundle\Repository\TrackEOnlineRepository;
29+
use Chamilo\CoreBundle\Security\Authorization\Voter\UserVoter;
2930
use Chamilo\CoreBundle\Serializer\UserToJsonNormalizer;
3031
use Chamilo\CoreBundle\Settings\SettingsManager;
3132
use Chamilo\CourseBundle\Repository\CForumThreadRepository;
@@ -49,6 +50,7 @@
4950
use Symfony\Contracts\Translation\TranslatorInterface;
5051
use UserManager;
5152

53+
#[IsGranted('ROLE_USER')]
5254
#[Route('/social-network')]
5355
class SocialController extends AbstractController
5456
{
@@ -178,8 +180,7 @@ public function getLegalTerms(
178180
#[Route('/legal-status/{userId}', name: 'chamilo_core_social_legal_status')]
179181
public function getLegalStatus(
180182
int $userId,
181-
#[CurrentUser]
182-
User $currentUser,
183+
#[CurrentUser] User $currentUser,
183184
SettingsManager $settingsManager,
184185
TranslatorInterface $translator,
185186
UserRepository $userRepo,
@@ -204,8 +205,17 @@ public function getLegalStatus(
204205
}
205206

206207
$isoCode = $currentUser->getLocale();
207-
$latestLanguageId = $this->resolveLegalLanguageId($languageRepo, $isoCode, $settingsManager);
208-
$latestVersion = 0 !== $latestLanguageId ? $legalTermsRepo->getLastVersionByLanguage($latestLanguageId) : null;
208+
209+
$resolved = $this->resolveLatestTermsVersion($isoCode, $languageRepo, $legalTermsRepo, $settingsManager);
210+
if (!$resolved) {
211+
return $this->json([
212+
'isAccepted' => false,
213+
'message' => $translator->trans('No terms and conditions available', [], 'messages'),
214+
], Response::HTTP_NOT_FOUND);
215+
}
216+
217+
$latestLanguageId = (int) $resolved['languageId'];
218+
$latestVersion = (int) $resolved['version'];
209219

210220
$extraFieldValue = new ExtraFieldValue('user');
211221
$value = $extraFieldValue->get_values_by_handler_and_field_variable($userId, 'legal_accept');
@@ -239,46 +249,59 @@ public function getLegalStatus(
239249
]);
240250
}
241251

242-
#[Route('/send-legal-term', name: 'chamilo_core_social_send_legal_term')]
252+
#[Route('/send-legal-term', name: 'chamilo_core_social_send_legal_term', methods: ['POST'])]
243253
public function sendLegalTerm(
244254
Request $request,
255+
#[CurrentUser] User $currentUser,
245256
SettingsManager $settingsManager,
246257
TranslatorInterface $translator,
247258
LegalRepository $legalTermsRepo,
248259
UserRepository $userRepo,
249260
LanguageRepository $languageRepo
250261
): JsonResponse {
251262
$data = json_decode($request->getContent(), true);
252-
$userId = $data['userId'] ?? null;
253-
254-
$user = $userRepo->find($userId);
255-
if (!$user) {
256-
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
263+
if (!is_array($data)) {
264+
return $this->json(['error' => 'Invalid JSON body'], Response::HTTP_BAD_REQUEST);
257265
}
258266

259-
$isoCode = $user->getLocale();
260-
$languageId = $this->resolveLegalLanguageId($languageRepo, $isoCode, $settingsManager);
261-
if (0 === $languageId) {
262-
return $this->json(['error' => 'Language not found'], Response::HTTP_BAD_REQUEST);
267+
$requestedUserId = isset($data['userId']) ? (int) $data['userId'] : 0;
268+
269+
// Non-admin users can only accept terms for themselves.
270+
if (!$this->isGranted('ROLE_ADMIN')) {
271+
$targetUserId = $currentUser->getId();
272+
$user = $currentUser;
273+
} else {
274+
$targetUserId = $requestedUserId > 0 ? $requestedUserId : 0;
275+
if (0 === $targetUserId) {
276+
return $this->json(['error' => 'Missing or invalid userId'], Response::HTTP_BAD_REQUEST);
277+
}
278+
$user = $userRepo->find($targetUserId);
279+
if (!$user) {
280+
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
281+
}
263282
}
264283

265-
$version = $legalTermsRepo->getLastVersionByLanguage($languageId);
266-
if (null === $version) {
284+
$isoCode = $user->getLocale();
285+
$resolved = $this->resolveLatestTermsVersion($isoCode, $languageRepo, $legalTermsRepo, $settingsManager);
286+
if (!$resolved) {
267287
return $this->json(['error' => 'Terms not found'], Response::HTTP_NOT_FOUND);
268288
}
289+
$languageId = (int) $resolved['languageId'];
290+
$version = (int) $resolved['version'];
269291

270-
$legalAcceptType = $version.':'.$languageId.':'.time();
271-
UserManager::update_extra_field_value((int) $userId, 'legal_accept', $legalAcceptType);
292+
$legalAcceptType = $version . ':' . $languageId . ':' . time();
293+
UserManager::update_extra_field_value($targetUserId, 'legal_accept', $legalAcceptType);
272294

273-
$bossList = UserManager::getStudentBossList((int) $userId);
295+
// Notify bosses (kept as-is, now based on $targetUserId)
296+
$bossList = UserManager::getStudentBossList($targetUserId);
274297
if (!empty($bossList)) {
275298
$bossList = array_column($bossList, 'boss_id');
276299
foreach ($bossList as $bossId) {
277-
$subjectEmail = \sprintf(
300+
$subjectEmail = sprintf(
278301
$translator->trans('User %s signed the agreement.', [], 'messages'),
279302
$user->getFullName()
280303
);
281-
$contentEmail = \sprintf(
304+
$contentEmail = sprintf(
282305
$translator->trans('User %s signed the agreement the %s.', [], 'messages'),
283306
$user->getFullName(),
284307
$this->dateTimeHelper->localTimeYmdHis(null, null, 'UTC')
@@ -288,7 +311,7 @@ public function sendLegalTerm(
288311
(int) $bossId,
289312
$subjectEmail,
290313
$contentEmail,
291-
(int) $userId
314+
$targetUserId
292315
);
293316
}
294317
}
@@ -299,88 +322,123 @@ public function sendLegalTerm(
299322
]);
300323
}
301324

302-
#[IsGranted('ROLE_ADMIN')]
303-
#[Route('/delete-legal', name: 'chamilo_core_social_delete_legal')]
325+
#[Route('/delete-legal', name: 'chamilo_core_social_delete_legal', methods: ['POST'])]
304326
public function deleteLegal(
305-
#[CurrentUser]
306-
User $currentUser,
327+
#[CurrentUser] User $currentUser,
307328
Request $request,
308329
TranslatorInterface $translator
309330
): JsonResponse {
310331
$data = json_decode($request->getContent(), true);
311-
$userId = $data['userId'] ?? null;
332+
if (!is_array($data)) {
333+
return $this->json(['error' => 'Invalid JSON body'], Response::HTTP_BAD_REQUEST);
334+
}
312335

313-
if ($userId !== $currentUser->getId()) {
314-
throw $this->createAccessDeniedException();
336+
// Backward compatible: if userId is not provided, default to current user.
337+
$requestedUserId = isset($data['userId']) ? (int) $data['userId'] : 0;
338+
339+
// Non-admin users can only revoke their own consent (ignore provided userId if different).
340+
if (!$this->isGranted('ROLE_ADMIN')) {
341+
$targetUserId = $currentUser->getId();
342+
} else {
343+
// Admin must explicitly provide a valid userId.
344+
$targetUserId = $requestedUserId > 0 ? $requestedUserId : 0;
345+
if (0 === $targetUserId) {
346+
return $this->json(['error' => 'Missing or invalid userId'], Response::HTTP_BAD_REQUEST);
347+
}
315348
}
316349

317350
$extraFieldValue = new ExtraFieldValue('user');
318-
$value = $extraFieldValue->get_values_by_handler_and_field_variable($userId, 'legal_accept');
319-
if ($value && isset($value['id'])) {
351+
352+
$value = $extraFieldValue->get_values_by_handler_and_field_variable($targetUserId, 'legal_accept');
353+
if (!empty($value['id'])) {
320354
$extraFieldValue->delete($value['id']);
321355
}
322356

323-
$value = $extraFieldValue->get_values_by_handler_and_field_variable($userId, 'termactivated');
324-
if ($value && isset($value['id'])) {
357+
$value = $extraFieldValue->get_values_by_handler_and_field_variable($targetUserId, 'termactivated');
358+
if (!empty($value['id'])) {
325359
$extraFieldValue->delete($value['id']);
326360
}
327361

328-
return $this->json(['success' => true, 'message' => $translator->trans('Legal consent revoked successfully.')]);
362+
return $this->json([
363+
'success' => true,
364+
'message' => $translator->trans('Legal consent revoked successfully.'),
365+
]);
329366
}
330367

331-
#[Route('/handle-privacy-request', name: 'chamilo_core_social_handle_privacy_request')]
368+
#[Route('/handle-privacy-request', name: 'chamilo_core_social_handle_privacy_request', methods: ['POST'])]
332369
public function handlePrivacyRequest(
333370
Request $request,
371+
#[CurrentUser] User $currentUser,
334372
SettingsManager $settingsManager,
335373
UserRepository $userRepo,
336374
TranslatorInterface $translator,
337-
MailerInterface $mailer,
338-
RequestStack $requestStack
375+
MailerInterface $mailer
339376
): JsonResponse {
340377
$data = json_decode($request->getContent(), true);
341-
$userId = $data['userId'] ? (int) $data['userId'] : null;
342-
$explanation = $data['explanation'] ?? '';
343-
$requestType = $data['requestType'] ?? '';
378+
if (!is_array($data)) {
379+
return $this->json(['success' => false, 'message' => 'Invalid JSON body'], Response::HTTP_BAD_REQUEST);
380+
}
344381

345-
/** @var User $user */
346-
$user = $userRepo->find($userId);
382+
$requestedUserId = isset($data['userId']) ? (int) $data['userId'] : 0;
383+
$explanation = (string) ($data['explanation'] ?? '');
384+
$requestType = (string) ($data['requestType'] ?? '');
347385

348-
if (!$user) {
349-
return $this->json(['success' => false, 'message' => 'User not found']);
386+
// Non-admin users can only create requests for themselves
387+
if (!$this->isGranted('ROLE_ADMIN')) {
388+
$targetUserId = $currentUser->getId();
389+
$user = $currentUser;
390+
} else {
391+
// Admin can create requests for another user if explicitly provided
392+
$targetUserId = $requestedUserId > 0 ? $requestedUserId : 0;
393+
if (0 === $targetUserId) {
394+
return $this->json(['success' => false, 'message' => 'Missing or invalid userId'], Response::HTTP_BAD_REQUEST);
395+
}
396+
$user = $userRepo->find($targetUserId);
397+
if (!$user) {
398+
return $this->json(['success' => false, 'message' => 'User not found'], Response::HTTP_NOT_FOUND);
399+
}
350400
}
351401

352-
$request = $requestStack->getCurrentRequest();
353-
$baseUrl = $request->getSchemeAndHttpHost().$request->getBasePath();
354-
$specificPath = '/main/admin/user_list_consent.php';
355-
$link = $baseUrl.$specificPath;
402+
$baseUrl = $request->getSchemeAndHttpHost() . $request->getBasePath();
403+
$link = $baseUrl . '/main/admin/user_list_consent.php';
356404

357405
if ('delete_account' === $requestType) {
358406
$fieldToUpdate = 'request_for_delete_account';
359407
$justificationFieldToUpdate = 'request_for_delete_account_justification';
360408
$emailSubject = $translator->trans('Request for account removal');
361-
$emailContent = \sprintf($translator->trans('User %s asked for the deletion of his/her account, explaining that "%s". You can process the request here: %s'), $user->getFullName(), $explanation, '<a href="'.$link.'">'.$link.'</a>');
409+
$emailContent = sprintf(
410+
$translator->trans('User %s asked for the deletion of his/her account, explaining that "%s". You can process the request here: %s'),
411+
$user->getFullName(),
412+
$explanation,
413+
'<a href="'.$link.'">'.$link.'</a>'
414+
);
362415
} elseif ('delete_legal' === $requestType) {
363416
$fieldToUpdate = 'request_for_legal_agreement_consent_removal';
364417
$justificationFieldToUpdate = 'request_for_legal_agreement_consent_removal_justification';
365418
$emailSubject = $translator->trans('Request for consent withdrawal on legal terms');
366-
$emailContent = \sprintf($translator->trans('User %s asked for the removal of his/her consent to our legal terms, explaining that "%s". You can process the request here: %s'), $user->getFullName(), $explanation, '<a href="'.$link.'">'.$link.'</a>');
419+
$emailContent = sprintf(
420+
$translator->trans('User %s asked for the removal of his/her consent to our legal terms, explaining that "%s". You can process the request here: %s'),
421+
$user->getFullName(),
422+
$explanation,
423+
'<a href="'.$link.'">'.$link.'</a>'
424+
);
367425
} else {
368-
return $this->json(['success' => false, 'message' => 'Invalid action type']);
426+
return $this->json(['success' => false, 'message' => 'Invalid action type'], Response::HTTP_BAD_REQUEST);
369427
}
370428

371429
UserManager::createDataPrivacyExtraFields();
372-
UserManager::update_extra_field_value($userId, $fieldToUpdate, 1);
373-
UserManager::update_extra_field_value($userId, $justificationFieldToUpdate, $explanation);
430+
UserManager::update_extra_field_value($targetUserId, $fieldToUpdate, 1);
431+
UserManager::update_extra_field_value($targetUserId, $justificationFieldToUpdate, $explanation);
432+
433+
$emailOfficer = (string) $settingsManager->getSetting('profile.data_protection_officer_email');
374434

375-
$emailOfficer = $settingsManager->getSetting('profile.data_protection_officer_email');
376-
if (!empty($emailOfficer)) {
377-
$email = new Email();
378-
$email
435+
if ('' !== trim($emailOfficer)) {
436+
$email = (new Email())
379437
->from($user->getEmail())
380438
->to($emailOfficer)
381439
->subject($emailSubject)
382-
->html($emailContent)
383-
;
440+
->html($emailContent);
441+
384442
$mailer->send($email);
385443
} else {
386444
MessageManager::sendMessageToAllAdminUsers($user->getId(), $emailSubject, $emailContent);
@@ -529,7 +587,6 @@ public function groupInvitedUsers(int $groupId, UsergroupRepository $usergroupRe
529587
return $this->json(['invitedUsers' => $invitedUsersList]);
530588
}
531589

532-
#[IsGranted('ROLE_USER')]
533590
#[Route('/user-profile/{userId}', name: 'chamilo_core_social_user_profile')]
534591
public function getUserProfile(
535592
int $userId,

src/CoreBundle/Repository/LegalRepository.php

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,17 @@ public function getLastVersion(int $languageId)
127127

128128
public function getLastVersionByLanguage(int $languageId): ?int
129129
{
130-
try {
131-
$result = $this->createQueryBuilder('l')
132-
->select('MAX(l.version)')
133-
->andWhere('l.languageId = :languageId')
134-
->setParameter('languageId', $languageId)
135-
->getQuery()
136-
->getSingleScalarResult()
137-
;
138-
139-
return null !== $result ? (int) $result : null;
140-
} catch (NonUniqueResultException|NoResultException) {
141-
return null;
142-
}
130+
$result = $this->createQueryBuilder('l')
131+
->select('MAX(l.version) as maxVersion')
132+
->andWhere('l.languageId = :languageId')
133+
->setParameter('languageId', $languageId)
134+
->getQuery()
135+
->getSingleScalarResult()
136+
;
137+
138+
$version = (int) $result;
139+
140+
return $version > 0 ? $version : null;
143141
}
144142

145143
/**

src/CoreBundle/Security/Authorization/Voter/UserVoter.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
6565
/** @var User $user */
6666
$user = $subject;
6767

68-
if (self::VIEW === $attribute) {
69-
// If the user is on the social page and is logged in, allow access
70-
if ($this->isFromSocialPage() && null !== $currentUser->getId()) {
71-
return true;
72-
}
68+
if (self::EDIT === $attribute) {
69+
// Only the owner can edit private data
70+
return (int) $currentUser->getId() === (int) $user->getId();
71+
}
7372

74-
if ($currentUser === $user) {
73+
if (self::VIEW === $attribute) {
74+
if ((int) $currentUser->getId() === (int) $user->getId()) {
7575
return true;
7676
}
7777

0 commit comments

Comments
 (0)