Skip to content

Commit 728b532

Browse files
committed
Security: ResourceFile: Add collection extension to enforce VIEW permissions on /api/resource_files endpoint
See advisory GHSA-rm3h-fm8x-8mjp
1 parent 05687c1 commit 728b532

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/* For licensing terms, see /license.txt */
6+
7+
namespace Chamilo\CoreBundle\DataProvider\Extension;
8+
9+
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
10+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
11+
use ApiPlatform\Metadata\Operation;
12+
use Chamilo\CoreBundle\Entity\Course;
13+
use Chamilo\CoreBundle\Entity\ResourceFile;
14+
use Chamilo\CoreBundle\Entity\ResourceLink;
15+
use Chamilo\CoreBundle\Entity\User;
16+
use Doctrine\ORM\QueryBuilder;
17+
use Symfony\Bundle\SecurityBundle\Security;
18+
19+
/**
20+
* Restricts the `/api/resource_files` collection to rows the requester may VIEW.
21+
*
22+
* Without this extension the GetCollection — gated only by ROLE_USER — would
23+
* return every row in `resource_file` to any authenticated user, leaking
24+
* uploader-supplied `originalName` values (frequently sensitive, e.g.
25+
* "firstname.lastname.cv.pdf") across the entire platform.
26+
*
27+
* Visibility rules walk through the `resourceNode` relation and mirror those
28+
* of {@see ResourceNodeExtension}:
29+
* - administrators see every file;
30+
* - authenticated users see a file when ANY of the following holds about
31+
* the parent ResourceNode:
32+
* * they created it,
33+
* * it has at least one ResourceLink targeting them as user,
34+
* * it has at least one ResourceLink whose linked course they belong to
35+
* (teacher or student via CourseRelUser),
36+
* * it has at least one published ResourceLink whose linked course is
37+
* OPEN_WORLD (public to any authenticated platform user).
38+
*/
39+
final class ResourceFileExtension implements QueryCollectionExtensionInterface
40+
{
41+
public function __construct(
42+
private readonly Security $security
43+
) {}
44+
45+
public function applyToCollection(
46+
QueryBuilder $queryBuilder,
47+
QueryNameGeneratorInterface $queryNameGenerator,
48+
string $resourceClass,
49+
?Operation $operation = null,
50+
array $context = []
51+
): void {
52+
if (ResourceFile::class !== $resourceClass) {
53+
return;
54+
}
55+
56+
if ($this->security->isGranted('ROLE_ADMIN')) {
57+
return;
58+
}
59+
60+
$user = $this->security->getUser();
61+
if (!$user instanceof User) {
62+
// GetCollection requires ROLE_USER; reaching this branch means an
63+
// unexpected token shape. Deny by returning an impossible predicate.
64+
$queryBuilder->andWhere('1 = 0');
65+
66+
return;
67+
}
68+
69+
$rootAlias = $queryBuilder->getRootAliases()[0];
70+
71+
$queryBuilder
72+
->leftJoin("$rootAlias.resourceNode", 'rf_acl_node')
73+
->leftJoin('rf_acl_node.resourceLinks', 'rf_acl_link')
74+
->leftJoin('rf_acl_link.course', 'rf_acl_course')
75+
->leftJoin('rf_acl_course.users', 'rf_acl_course_user')
76+
->andWhere(
77+
$queryBuilder->expr()->orX(
78+
'rf_acl_node.creator = :rf_acl_user',
79+
'rf_acl_link.user = :rf_acl_user',
80+
'rf_acl_course_user.user = :rf_acl_user',
81+
$queryBuilder->expr()->andX(
82+
'rf_acl_course.visibility = :rf_acl_open_world',
83+
'rf_acl_link.visibility = :rf_acl_published'
84+
)
85+
)
86+
)
87+
->setParameter('rf_acl_user', $user->getId())
88+
->setParameter('rf_acl_open_world', Course::OPEN_WORLD)
89+
->setParameter('rf_acl_published', ResourceLink::VISIBILITY_PUBLISHED)
90+
->distinct()
91+
;
92+
}
93+
}

0 commit comments

Comments
 (0)