Skip to content

Commit

Permalink
AnnotationController: fix improper authorization vulnerability
Browse files Browse the repository at this point in the history
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 <jeremy.benoist@gmail.com>
Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
  • Loading branch information
Kdecherf and j0k3r committed Jan 27, 2023
1 parent 9e9aede commit 3ed7f2b
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 61 deletions.
Expand Up @@ -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;
Expand All @@ -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];

Expand Down Expand Up @@ -72,50 +72,71 @@ 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);
}
}

/**
* Removes an annotation.
*
* @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;
}
}
Expand Up @@ -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();
}

Expand Down
22 changes: 20 additions & 2 deletions src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php
Expand Up @@ -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.
*
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -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();

Expand Down
Expand Up @@ -22,8 +22,6 @@ public function dataForEachAnnotations()
}

/**
* Test fetching annotations for an entry.
*
* @dataProvider dataForEachAnnotations
*/
public function testGetAnnotations($prefixUrl)
Expand All @@ -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');
Expand All @@ -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');
}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -195,8 +206,6 @@ public function testSetAnnotationWithQuoteTooLong($prefixUrl)
}

/**
* Test editing an existing annotation.
*
* @dataProvider dataForEachAnnotations
*/
public function testEditAnnotation($prefixUrl)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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());
}
}

0 comments on commit 3ed7f2b

Please sign in to comment.