22
33namespace Drupal \graphql \Routing ;
44
5+ use Asm89 \Stack \CorsService ;
56use Drupal \Component \Utility \NestedArray ;
67use Drupal \Core \Routing \EnhancerInterface ;
78use Drupal \Core \Routing \RouteObjectInterface ;
89use Drupal \graphql \Utility \JsonHelper ;
910use GraphQL \Server \Helper ;
1011use Symfony \Component \HttpFoundation \Request ;
11-
12+ use Symfony \ Component \ HttpKernel \ Exception \ BadRequestHttpException ;
1213
1314class QueryRouteEnhancer implements EnhancerInterface {
1415
16+ /**
17+ * The CORS options for Origin header checking.
18+ *
19+ * @var array
20+ */
21+ protected $ corsOptions ;
22+
23+ /**
24+ * Constructor.
25+ */
26+ public function __construct (array $ corsOptions ) {
27+ $ this ->corsOptions = $ corsOptions ;
28+ }
29+
1530 /**
1631 * {@inheritdoc}
1732 */
@@ -21,6 +36,10 @@ public function enhance(array $defaults, Request $request) {
2136 return $ defaults ;
2237 }
2338
39+ if ($ request ->getMethod () === "POST " ) {
40+ $ this ->assertValidPostRequestHeaders ($ request );
41+ }
42+
2443 $ helper = new Helper ();
2544 $ method = $ request ->getMethod ();
2645 $ body = $ this ->extractBody ($ request );
@@ -37,8 +56,89 @@ public function enhance(array $defaults, Request $request) {
3756 }
3857
3958 return $ defaults + [
40- 'operations ' => $ operations ,
41- ];
59+ 'operations ' => $ operations ,
60+ ];
61+ }
62+
63+ /**
64+ * Ensures that the headers for a POST request have triggered a preflight.
65+ *
66+ * POST requests must be submitted with content-type headers that properly
67+ * trigger a cross-origin preflight request. In case content-headers are used
68+ * that would trigger a "simple" request then custom headers must be provided.
69+ *
70+ * @param \Symfony\Component\HttpFoundation\Request $request
71+ * The request to check.
72+ *
73+ * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
74+ * In case the headers indicated a preflight was not performed.
75+ */
76+ protected function assertValidPostRequestHeaders (Request $ request ): void {
77+ $ content_type = $ request ->headers ->get ('content-type ' );
78+ if ($ content_type === NULL ) {
79+ throw new BadRequestHttpException ("GraphQL requests must specify a valid content type header. " );
80+ }
81+
82+ // application/graphql is a non-standard header that's supported by our
83+ // server implementation and triggers CORS.
84+ if ($ content_type === "application/graphql " ) {
85+ return ;
86+ }
87+
88+ /** @phpstan-ignore-next-line */
89+ $ content_format = method_exists ($ request , 'getContentTypeFormat ' ) ? $ request ->getContentTypeFormat () : $ request ->getContentType ();
90+ if ($ content_format === NULL ) {
91+ // Symfony before 5.4 does not detect "multipart/form-data", check for it
92+ // manually.
93+ if (stripos ($ content_type , 'multipart/form-data ' ) === 0 ) {
94+ $ content_format = 'form ' ;
95+ }
96+ else {
97+ throw new BadRequestHttpException ("The content type ' $ content_type' is not supported. " );
98+ }
99+ }
100+
101+ // JSON requests provide a non-standard header that trigger CORS.
102+ if ($ content_format === "json " ) {
103+ return ;
104+ }
105+
106+ // The form content types are considered simple requests and don't trigger
107+ // CORS pre-flight checks, so these require a separate header to prevent
108+ // CSRF. We need to support "form" for file uploads.
109+ if ($ content_format === "form " ) {
110+ // If the client set a custom header then we can be sure CORS was
111+ // respected.
112+ $ custom_headers = [
113+ 'Apollo-Require-Preflight ' ,
114+ 'X-Apollo-Operation-Name ' ,
115+ 'x-graphql-yoga-csrf ' ,
116+ ];
117+ foreach ($ custom_headers as $ custom_header ) {
118+ if ($ request ->headers ->has ($ custom_header )) {
119+ return ;
120+ }
121+ }
122+ // 1. Allow requests that have set no Origin header at all, for example
123+ // server-to-server requests.
124+ // 2. Allow requests where the Origin matches the site's domain name.
125+ $ origin = $ request ->headers ->get ('Origin ' );
126+ if ($ origin === NULL || $ request ->getSchemeAndHttpHost () === $ origin ) {
127+ return ;
128+ }
129+ // Allow other origins as configured in the CORS policy.
130+ if (!empty ($ this ->corsOptions ['enabled ' ])) {
131+ $ cors_service = new CorsService ($ this ->corsOptions );
132+ // Drupal 9 compatibility, method name has changed in Drupal 10.
133+ /** @phpstan-ignore-next-line */
134+ if ($ cors_service ->isActualRequestAllowed ($ request )) {
135+ return ;
136+ }
137+ }
138+ throw new BadRequestHttpException ("Form requests must include a Apollo-Require-Preflight HTTP header or the Origin HTTP header value needs to be in the allowedOrigins in the CORS settings. " );
139+ }
140+
141+ throw new BadRequestHttpException ("The content type ' $ content_type' is not supported. " );
42142 }
43143
44144 /**
0 commit comments