Skip to content

Commit 328cb67

Browse files
authored
fix(symfony): don't expose entrypoint in openapi format (#8383)
Fixes #8361
1 parent 340e982 commit 328cb67

6 files changed

Lines changed: 148 additions & 12 deletions

File tree

src/Metadata/Util/ContentNegotiationTrait.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ private function getRequestFormat(Request $request, array $formats, bool $throw
119119
if (null !== $requestFormat) {
120120
$mimeType = $request->getMimeType($requestFormat);
121121

122-
if (isset($flattenedMimeTypes[$mimeType])) {
122+
if (null !== $mimeType && isset($flattenedMimeTypes[$mimeType])) {
123123
return $requestFormat;
124124
}
125125

126126
if ($throw) {
127-
throw $this->getNotAcceptableHttpException($mimeType, $flattenedMimeTypes);
127+
throw $this->getNotAcceptableHttpException($mimeType ?? $requestFormat, $flattenedMimeTypes);
128128
}
129129
}
130130

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,11 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
365365
$container->setParameter('api_platform.patch_formats', $patchFormats);
366366
$container->setParameter('api_platform.error_formats', $errorFormats);
367367
$container->setParameter('api_platform.docs_formats', $docsFormats);
368+
// The entrypoint only has normalizers for hypermedia formats (jsonld, jsonhal, jsonapi) and a
369+
// dedicated Swagger UI code path for html. Other documentation formats (e.g. openapi, yamlopenapi)
370+
// have no Entrypoint normalizer and must not be advertised, otherwise content negotiation lets them
371+
// through and the Symfony ObjectNormalizer fallback leaks the internal ResourceNameCollection FQCNs.
372+
$container->setParameter('api_platform.entrypoint_formats', array_intersect_key($docsFormats, array_flip(['jsonld', 'jsonhal', 'jsonapi', 'html'])));
368373
$container->setParameter('api_platform.jsonschema_formats', []);
369374
$container->setParameter('api_platform.eager_loading.enabled', $this->isConfigEnabled($container, $config['eager_loading']));
370375
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);

src/Symfony/Bundle/Resources/config/symfony/controller.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
service('api_platform.metadata.resource.name_collection_factory'),
3737
service('api_platform.state_provider.main'),
3838
service('api_platform.state_processor.main'),
39-
'%api_platform.docs_formats%',
39+
'%api_platform.entrypoint_formats%',
4040
]);
4141

4242
$services->set('api_platform.action.documentation', DocumentationAction::class)

src/Symfony/Bundle/Resources/config/symfony/events.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@
184184
service('api_platform.metadata.resource.name_collection_factory'),
185185
service('api_platform.state_provider.documentation'),
186186
service('api_platform.state_processor.documentation'),
187-
'%api_platform.docs_formats%',
187+
'%api_platform.entrypoint_formats%',
188188
]);
189189

190190
$services->set('api_platform.action.documentation', DocumentationAction::class)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Functional;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MultipleResourceBook;
18+
use ApiPlatform\Tests\SetupClassResourcesTrait;
19+
20+
/**
21+
* The entrypoint must only expose hypermedia formats that have a dedicated
22+
* EntrypointNormalizer (jsonld, jsonhal, jsonapi). Documentation formats
23+
* (openapi, html) have no such normalizer and must not be served, otherwise
24+
* the Symfony ObjectNormalizer fallback dumps the public ResourceNameCollection,
25+
* leaking internal PHP FQCNs.
26+
*
27+
* @see https://github.com/api-platform/core/issues/8361
28+
*/
29+
final class EntrypointFormatTest extends ApiTestCase
30+
{
31+
use SetupClassResourcesTrait;
32+
33+
protected static ?bool $alwaysBootKernel = false;
34+
35+
/**
36+
* @return class-string[]
37+
*/
38+
public static function getResources(): array
39+
{
40+
return [MultipleResourceBook::class];
41+
}
42+
43+
public function testEntrypointRejectsOpenApiAcceptHeader(): void
44+
{
45+
$response = self::createClient()->request('GET', '/', [
46+
'headers' => ['accept' => 'application/vnd.openapi+json'],
47+
]);
48+
49+
$this->assertResponseStatusCodeSame(406);
50+
}
51+
52+
/**
53+
* The ".jsonopenapi"/".yamlopenapi" URL suffixes are resolved by routing before
54+
* content negotiation runs, so an unsupported route format yields a 404
55+
* (consistent with any other resource requested with an unsupported format
56+
* suffix), not a 406.
57+
*/
58+
public function testEntrypointRejectsOpenApiFormatSuffix(): void
59+
{
60+
$response = self::createClient()->request('GET', '/index.jsonopenapi', [
61+
'headers' => ['accept' => 'application/vnd.openapi+json'],
62+
]);
63+
64+
$this->assertResponseStatusCodeSame(404);
65+
}
66+
67+
public function testEntrypointRejectsYamlOpenApiFormatSuffix(): void
68+
{
69+
$response = self::createClient()->request('GET', '/index.yamlopenapi', [
70+
'headers' => ['accept' => 'application/vnd.openapi+yaml'],
71+
]);
72+
73+
$this->assertResponseStatusCodeSame(404);
74+
}
75+
76+
public function testEntrypointStillServesHtmlAsSwaggerUi(): void
77+
{
78+
$response = self::createClient()->request('GET', '/index.html', [
79+
'headers' => ['accept' => 'text/html'],
80+
]);
81+
82+
$this->assertResponseIsSuccessful();
83+
$this->assertStringContainsString('swagger-ui', $response->getContent());
84+
}
85+
86+
public function testEntrypointStillServesJsonLd(): void
87+
{
88+
$response = self::createClient()->request('GET', '/index.jsonld', [
89+
'headers' => ['accept' => 'application/ld+json'],
90+
]);
91+
92+
$this->assertResponseIsSuccessful();
93+
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
94+
$data = $response->toArray();
95+
$this->assertArrayHasKey('multipleResourceBook2', $data);
96+
$this->assertEquals('/multi_route_books', $data['multipleResourceBook2']);
97+
$this->assertStringNotContainsString('resourceNameCollection', $response->getContent());
98+
}
99+
100+
public function testEntrypointStillServesJsonHal(): void
101+
{
102+
$response = self::createClient()->request('GET', '/index.jsonhal', [
103+
'headers' => ['accept' => 'application/hal+json'],
104+
]);
105+
106+
$this->assertResponseIsSuccessful();
107+
$this->assertResponseHeaderSame('content-type', 'application/hal+json; charset=utf-8');
108+
$data = $response->toArray();
109+
$this->assertArrayHasKey('_links', $data);
110+
$this->assertArrayHasKey('multipleResourceBook2', $data['_links']);
111+
}
112+
113+
public function testEntrypointStillServesJsonApi(): void
114+
{
115+
$response = self::createClient()->request('GET', '/index.jsonapi', [
116+
'headers' => ['accept' => 'application/vnd.api+json'],
117+
]);
118+
119+
$this->assertResponseIsSuccessful();
120+
$this->assertResponseHeaderSame('content-type', 'application/vnd.api+json; charset=utf-8');
121+
$data = $response->toArray();
122+
$this->assertArrayHasKey('links', $data);
123+
$this->assertArrayHasKey('multipleResourceBook2', $data['links']);
124+
}
125+
}

tests/Functional/OpenApiTest.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -686,24 +686,30 @@ public function testOpenApiUiIsEnabledForDocsEndpointWithDummyObject(): void
686686
$this->assertResponseIsSuccessful();
687687
}
688688

689-
public function testRetrieveTheEntrypoint(): void
689+
/**
690+
* @see https://github.com/api-platform/core/issues/8361
691+
*/
692+
public function testEntrypointRejectsOpenApiFormat(): void
690693
{
691694
$response = self::createClient()->request('GET', '/', [
692695
'headers' => ['Accept' => 'application/vnd.openapi+json'],
693696
]);
694-
$this->assertResponseIsSuccessful();
695-
$this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8');
696-
$this->assertJson($response->getContent());
697+
$this->assertResponseStatusCodeSame(406);
697698
}
698699

699-
public function testRetrieveTheEntrypointWithUrlFormat(): void
700+
/**
701+
* The ".jsonopenapi" URL suffix is resolved by routing before content negotiation
702+
* runs, so an unsupported route format yields a 404 (consistent with any other
703+
* resource requested with an unsupported format suffix), not a 406.
704+
*
705+
* @see https://github.com/api-platform/core/issues/8361
706+
*/
707+
public function testEntrypointRejectsOpenApiFormatWithUrlFormat(): void
700708
{
701709
$response = self::createClient()->request('GET', '/index.jsonopenapi', [
702710
'headers' => ['Accept' => 'application/vnd.openapi+json'],
703711
]);
704-
$this->assertResponseIsSuccessful();
705-
$this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8');
706-
$this->assertJson($response->getContent());
712+
$this->assertResponseStatusCodeSame(404);
707713
}
708714

709715
public function testOpenApiSchemaWithNormalizationAttributes(): void

0 commit comments

Comments
 (0)