From 3ed7f2b7511607bd2cdfc4e8eacdfd44e4de3b41 Mon Sep 17 00:00:00 2001 From: Kevin Decherf Date: Mon, 23 Jan 2023 12:16:09 +0100 Subject: [PATCH] AnnotationController: fix improper authorization vulnerability This PR is based on 2.5.x branch. We fix the improper authorization by retrieving the annotation using id and user id. We also replace the ParamConverter used to get the requested Annotation on put and delete actions with an explicit call to AnnotationRepository in order to prevent a resource enumeration through response discrepancy. Fixes GHSA-mrqx-mjc4-vfh3 Co-authored-by: Jeremy Benoist Signed-off-by: Kevin Decherf --- .../WallabagAnnotationController.php | 73 +++++++---- .../DataFixtures/AnnotationFixtures.php | 9 ++ .../Repository/AnnotationRepository.php | 22 +++- .../Controller/AnnotationRestController.php | 9 +- .../Controller/AnnotationControllerTest.php | 114 ++++++++++++++---- .../Controller/ConfigControllerTest.php | 6 +- 6 files changed, 172 insertions(+), 61 deletions(-) diff --git a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php index 883ce4a89a..c94c58d84f 100644 --- a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php +++ b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php @@ -3,9 +3,9 @@ namespace Wallabag\AnnotationBundle\Controller; use FOS\RestBundle\Controller\FOSRestController; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Wallabag\AnnotationBundle\Entity\Annotation; use Wallabag\AnnotationBundle\Form\EditAnnotationType; use Wallabag\AnnotationBundle\Form\NewAnnotationType; @@ -25,7 +25,7 @@ public function getAnnotationsAction(Entry $entry) $annotationRows = $this ->getDoctrine() ->getRepository('WallabagAnnotationBundle:Annotation') - ->findAnnotationsByPageId($entry->getId(), $this->getUser()->getId()); + ->findByEntryIdAndUserId($entry->getId(), $this->getUser()->getId()); $total = \count($annotationRows); $annotations = ['total' => $total, 'rows' => $annotationRows]; @@ -72,31 +72,35 @@ public function postAnnotationAction(Request $request, Entry $entry) * * @see Wallabag\ApiBundle\Controller\WallabagRestController * - * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") - * * @return JsonResponse */ - public function putAnnotationAction(Annotation $annotation, Request $request) + public function putAnnotationAction(Request $request, int $annotation) { - $data = json_decode($request->getContent(), true); + try { + $annotation = $this->validateAnnotation($annotation, $this->getUser()->getId()); - $form = $this->get('form.factory')->createNamed('', EditAnnotationType::class, $annotation, [ - 'csrf_protection' => false, - 'allow_extra_fields' => true, - ]); - $form->submit($data); + $data = json_decode($request->getContent(), true, 512, \JSON_THROW_ON_ERROR); - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - $em->persist($annotation); - $em->flush(); + $form = $this->get('form.factory')->createNamed('', EditAnnotationType::class, $annotation, [ + 'csrf_protection' => false, + 'allow_extra_fields' => true, + ]); + $form->submit($data); - $json = $this->get('jms_serializer')->serialize($annotation, 'json'); + if ($form->isValid()) { + $em = $this->getDoctrine()->getManager(); + $em->persist($annotation); + $em->flush(); - return JsonResponse::fromJsonString($json); - } + $json = $this->get('jms_serializer')->serialize($annotation, 'json'); - return $form; + return JsonResponse::fromJsonString($json); + } + + return $form; + } catch (\InvalidArgumentException $e) { + throw new NotFoundHttpException($e); + } } /** @@ -104,18 +108,35 @@ public function putAnnotationAction(Annotation $annotation, Request $request) * * @see Wallabag\ApiBundle\Controller\WallabagRestController * - * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") - * * @return JsonResponse */ - public function deleteAnnotationAction(Annotation $annotation) + public function deleteAnnotationAction(int $annotation) + { + try { + $annotation = $this->validateAnnotation($annotation, $this->getUser()->getId()); + + $em = $this->getDoctrine()->getManager(); + $em->remove($annotation); + $em->flush(); + + $json = $this->get('jms_serializer')->serialize($annotation, 'json'); + + return (new JsonResponse())->setJson($json); + } catch (\InvalidArgumentException $e) { + throw new NotFoundHttpException($e); + } + } + + private function validateAnnotation(int $annotationId, int $userId) { $em = $this->getDoctrine()->getManager(); - $em->remove($annotation); - $em->flush(); - $json = $this->get('jms_serializer')->serialize($annotation, 'json'); + $annotation = $em->getRepository('WallabagAnnotationBundle:Annotation')->findOneByIdAndUserId($annotationId, $userId); - return (new JsonResponse())->setJson($json); + if (null === $annotation) { + throw new NotFoundHttpException(); + } + + return $annotation; } } diff --git a/src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php b/src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php index 51c79c3d3b..5161a40947 100644 --- a/src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php +++ b/src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php @@ -34,6 +34,15 @@ public function load(ObjectManager $manager) $this->addReference('annotation2', $annotation2); + $annotation3 = new Annotation($this->getReference('bob-user')); + $annotation3->setEntry($this->getReference('entry3')); + $annotation3->setText('This is my first annotation !'); + $annotation3->setQuote('content'); + + $manager->persist($annotation3); + + $this->addReference('annotation3', $annotation3); + $manager->flush(); } diff --git a/src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php b/src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php index 0de5c934e8..560f9eaf2b 100644 --- a/src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php +++ b/src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php @@ -41,6 +41,24 @@ public function findAnnotationById($annotationId) ; } + /** + * Find annotation by id and user. + * + * @param int $annotationId + * @param int $userId + * + * @return Annotation + */ + public function findOneByIdAndUserId($annotationId, $userId) + { + return $this->createQueryBuilder('a') + ->where('a.id = :annotationId')->setParameter('annotationId', $annotationId) + ->andWhere('a.user = :userId')->setParameter('userId', $userId) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + } + /** * Find annotations for entry id. * @@ -49,7 +67,7 @@ public function findAnnotationById($annotationId) * * @return array */ - public function findAnnotationsByPageId($entryId, $userId) + public function findByEntryIdAndUserId($entryId, $userId) { return $this->createQueryBuilder('a') ->where('a.entry = :entryId')->setParameter('entryId', $entryId) @@ -66,7 +84,7 @@ public function findAnnotationsByPageId($entryId, $userId) * * @return array */ - public function findLastAnnotationByPageId($entryId, $userId) + public function findLastAnnotationByUserId($entryId, $userId) { return $this->createQueryBuilder('a') ->where('a.entry = :entryId')->setParameter('entryId', $entryId) diff --git a/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php b/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php index 66693189ac..7061845d3a 100644 --- a/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php +++ b/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php @@ -3,7 +3,6 @@ namespace Wallabag\ApiBundle\Controller; use Nelmio\ApiDocBundle\Annotation\ApiDoc; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Wallabag\AnnotationBundle\Entity\Annotation; @@ -63,11 +62,9 @@ public function postAnnotationAction(Request $request, Entry $entry) * } * ) * - * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") - * * @return JsonResponse */ - public function putAnnotationAction(Annotation $annotation, Request $request) + public function putAnnotationAction(int $annotation, Request $request) { $this->validateAuthentication(); @@ -86,11 +83,9 @@ public function putAnnotationAction(Annotation $annotation, Request $request) * } * ) * - * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") - * * @return JsonResponse */ - public function deleteAnnotationAction(Annotation $annotation) + public function deleteAnnotationAction(int $annotation) { $this->validateAuthentication(); diff --git a/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php b/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php index 260edd770f..2e2ad032cf 100644 --- a/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php +++ b/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php @@ -22,8 +22,6 @@ public function dataForEachAnnotations() } /** - * Test fetching annotations for an entry. - * * @dataProvider dataForEachAnnotations */ public function testGetAnnotations($prefixUrl) @@ -35,15 +33,7 @@ public function testGetAnnotations($prefixUrl) ->findOneByUserName('admin'); $entry = $em ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUsernameAndNotArchived('admin'); - - $annotation = new Annotation($user); - $annotation->setEntry($entry); - $annotation->setText('This is my annotation /o/'); - $annotation->setQuote('content'); - - $em->persist($annotation); - $em->flush(); + ->findByUrlAndUserId('http://0.0.0.0/entry1', $user->getId()); if ('annotations' === $prefixUrl) { $this->logInAs('admin'); @@ -54,23 +44,44 @@ public function testGetAnnotations($prefixUrl) $content = json_decode($this->client->getResponse()->getContent(), true); $this->assertGreaterThanOrEqual(1, $content['total']); - $this->assertSame($annotation->getText(), $content['rows'][0]['text']); + } - // we need to re-fetch the annotation becase after the flush, it has been "detached" from the entity manager - $annotation = $em->getRepository('WallabagAnnotationBundle:Annotation')->findAnnotationById($annotation->getId()); - $em->remove($annotation); - $em->flush(); + /** + * @dataProvider dataForEachAnnotations + */ + public function testGetAnnotationsFromAnOtherUser($prefixUrl) + { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + + $otherUser = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUserName('bob'); + $entry = $em + ->getRepository('WallabagCoreBundle:Entry') + ->findByUrlAndUserId('http://0.0.0.0/entry3', $otherUser->getId()); + + if ('annotations' === $prefixUrl) { + $this->logInAs('admin'); + } + + $this->client->request('GET', $prefixUrl . '/' . $entry->getId() . '.json'); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + $this->assertGreaterThanOrEqual(0, $content['total']); } /** - * Test creating an annotation for an entry. - * * @dataProvider dataForEachAnnotations */ public function testSetAnnotation($prefixUrl) { $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUserName('admin'); + if ('annotations' === $prefixUrl) { $this->logInAs('admin'); } @@ -102,7 +113,7 @@ public function testSetAnnotation($prefixUrl) /** @var Annotation $annotation */ $annotation = $em ->getRepository('WallabagAnnotationBundle:Annotation') - ->findLastAnnotationByPageId($entry->getId(), 1); + ->findLastAnnotationByUserId($entry->getId(), $user->getId()); $this->assertSame('my annotation', $annotation->getText()); } @@ -195,8 +206,6 @@ public function testSetAnnotationWithQuoteTooLong($prefixUrl) } /** - * Test editing an existing annotation. - * * @dataProvider dataForEachAnnotations */ public function testEditAnnotation($prefixUrl) @@ -243,8 +252,31 @@ public function testEditAnnotation($prefixUrl) } /** - * Test deleting an annotation. - * + * @dataProvider dataForEachAnnotations + */ + public function testEditAnnotationFromAnOtherUser($prefixUrl) + { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + + $otherUser = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUserName('bob'); + $entry = $em + ->getRepository('WallabagCoreBundle:Entry') + ->findByUrlAndUserId('http://0.0.0.0/entry3', $otherUser->getId()); + $annotation = $em + ->getRepository('WallabagAnnotationBundle:Annotation') + ->findLastAnnotationByUserId($entry->getId(), $otherUser->getId()); + + $headers = ['CONTENT_TYPE' => 'application/json']; + $content = json_encode([ + 'text' => 'a modified annotation', + ]); + $this->client->request('PUT', $prefixUrl . '/' . $annotation->getId() . '.json', [], [], $headers, $content); + $this->assertSame(404, $this->client->getResponse()->getStatusCode()); + } + + /** * @dataProvider dataForEachAnnotations */ public function testDeleteAnnotation($prefixUrl) @@ -287,4 +319,40 @@ public function testDeleteAnnotation($prefixUrl) $this->assertNull($annotationDeleted); } + + /** + * @dataProvider dataForEachAnnotations + */ + public function testDeleteAnnotationFromAnOtherUser($prefixUrl) + { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + + $otherUser = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUserName('bob'); + $entry = $em + ->getRepository('WallabagCoreBundle:Entry') + ->findByUrlAndUserId('http://0.0.0.0/entry3', $otherUser->getId()); + $annotation = $em + ->getRepository('WallabagAnnotationBundle:Annotation') + ->findLastAnnotationByUserId($entry->getId(), $otherUser->getId()); + + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUserName('admin'); + $entry = $em + ->getRepository('WallabagCoreBundle:Entry') + ->findOneByUsernameAndNotArchived('admin'); + + if ('annotations' === $prefixUrl) { + $this->logInAs('admin'); + } + + $headers = ['CONTENT_TYPE' => 'application/json']; + $content = json_encode([ + 'text' => 'a modified annotation', + ]); + $this->client->request('DELETE', $prefixUrl . '/' . $annotation->getId() . '.json', [], [], $headers, $content); + $this->assertSame(404, $this->client->getResponse()->getStatusCode()); + } } diff --git a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php index acd8d58e7d..d6a0f0d780 100644 --- a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php @@ -932,7 +932,7 @@ public function testReset() $annotationsReset = $em ->getRepository('WallabagAnnotationBundle:Annotation') - ->findAnnotationsByPageId($entry->getId(), $user->getId()); + ->findByEntryIdAndUserId($entry->getId(), $user->getId()); $this->assertEmpty($annotationsReset, 'Annotations were reset'); @@ -1040,7 +1040,7 @@ public function testResetArchivedEntries() $annotationsReset = $em ->getRepository('WallabagAnnotationBundle:Annotation') - ->findAnnotationsByPageId($annotationArchived->getId(), $user->getId()); + ->findByEntryIdAndUserId($annotationArchived->getId(), $user->getId()); $this->assertEmpty($annotationsReset, 'Annotations were reset'); } @@ -1097,7 +1097,7 @@ public function testResetEntriesCascade() $annotationsReset = $em ->getRepository('WallabagAnnotationBundle:Annotation') - ->findAnnotationsByPageId($entry->getId(), $user->getId()); + ->findByEntryIdAndUserId($entry->getId(), $user->getId()); $this->assertEmpty($annotationsReset, 'Annotations were reset'); }