Skip to content

Commit aa52393

Browse files
authored
Security: Enforce source-course authorization in Course Maintenance copy endpoints
Refs GHSA-7g97-jh3w-53wh
1 parent 6b73ca8 commit aa52393

1 file changed

Lines changed: 32 additions & 9 deletions

File tree

src/CoreBundle/Controller/CourseMaintenanceController.php

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
namespace Chamilo\CoreBundle\Controller;
88

9+
use Chamilo\CoreBundle\Entity\Course as CourseEntity;
910
use Chamilo\CoreBundle\Repository\Node\UserRepository;
11+
use Chamilo\CoreBundle\Security\Authorization\Voter\CourseVoter;
1012
use Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Builder\Cc13Capabilities;
1113
use Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Builder\Cc13Export;
1214
use Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Import\Imscc13Import;
@@ -24,6 +26,7 @@
2426
use Symfony\Component\HttpFoundation\JsonResponse;
2527
use Symfony\Component\HttpFoundation\Request;
2628
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
29+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
2730
use Symfony\Component\Routing\Attribute\Route;
2831
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
2932
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -369,7 +372,7 @@ public function copyOptions(int $node, Request $req): JsonResponse
369372
}
370373

371374
#[Route('/copy/resources', name: 'copy_resources', methods: ['GET'])]
372-
public function copyResources(int $node, Request $req): JsonResponse
375+
public function copyResources(int $node, Request $req, EntityManagerInterface $em): JsonResponse
373376
{
374377
$this->setDebugFromRequest($req);
375378

@@ -378,6 +381,8 @@ public function copyResources(int $node, Request $req): JsonResponse
378381
return $this->jsonError('Missing sourceCourseId', 400);
379382
}
380383

384+
$this->assertSourceCourseAccess($sourceCourseCode, $em);
385+
381386
$cb = new CourseBuilder();
382387
$cb->set_tools_to_build(self::LEGACY_SNAPSHOT_TOOLS);
383388

@@ -391,22 +396,24 @@ public function copyResources(int $node, Request $req): JsonResponse
391396
}
392397

393398
#[Route('/copy/execute', name: 'copy_execute', methods: ['POST'])]
394-
public function copyExecute(int $node, Request $req): JsonResponse
399+
public function copyExecute(int $node, Request $req, EntityManagerInterface $em): JsonResponse
395400
{
396401
$this->setDebugFromRequest($req);
397402

398-
try {
399-
$payload = $this->getJsonPayload($req);
403+
$payload = $this->getJsonPayload($req);
400404

401-
$sourceCourseId = (string) ($payload['sourceCourseId'] ?? '');
405+
$sourceCourseId = (string) ($payload['sourceCourseId'] ?? '');
406+
if ('' === $sourceCourseId) {
407+
return $this->jsonError('Missing sourceCourseId', 400);
408+
}
409+
410+
$this->assertSourceCourseAccess($sourceCourseId, $em);
411+
412+
try {
402413
$copyOption = (string) ($payload['copyOption'] ?? 'full_copy'); // full_copy | select_items
403414
$sameFileNameOption = (int) ($payload['sameFileNameOption'] ?? 2);
404415
$selectedResourcesMap = (array) ($payload['resources'] ?? []);
405416

406-
if ('' === $sourceCourseId) {
407-
return $this->jsonError('Missing sourceCourseId', 400);
408-
}
409-
410417
$cb = new CourseBuilder('partial');
411418
$cb->set_tools_to_build(self::LEGACY_SNAPSHOT_TOOLS);
412419

@@ -1131,6 +1138,22 @@ public function importDiagnose(int $node, string $backupId, Request $req): JsonR
11311138
}
11321139
}
11331140

1141+
/**
1142+
* Resolve the source course by its code and ensure the current user is
1143+
* authorized to view (and therefore copy from) it. The request listener
1144+
* only gates the destination course via cid; the user-supplied source
1145+
* course must be authorized explicitly to prevent cross-course access.
1146+
*/
1147+
private function assertSourceCourseAccess(string $courseCode, EntityManagerInterface $em): void
1148+
{
1149+
$course = $em->getRepository(CourseEntity::class)->findOneBy(['code' => $courseCode]);
1150+
if (!$course instanceof CourseEntity) {
1151+
throw new NotFoundHttpException('Source course not found');
1152+
}
1153+
1154+
$this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
1155+
}
1156+
11341157
/**
11351158
* Map UI options (1/2/3) to legacy file policy.
11361159
*/

0 commit comments

Comments
 (0)