2
2
3
3
namespace Drupal \graphql \Routing ;
4
4
5
+ use Asm89 \Stack \CorsService ;
5
6
use Drupal \Component \Utility \NestedArray ;
6
7
use Drupal \Core \Routing \EnhancerInterface ;
7
8
use Drupal \Core \Routing \RouteObjectInterface ;
8
9
use Drupal \graphql \Utility \JsonHelper ;
9
10
use GraphQL \Server \Helper ;
10
11
use Symfony \Component \HttpFoundation \Request ;
11
-
12
+ use Symfony \ Component \ HttpKernel \ Exception \ BadRequestHttpException ;
12
13
13
14
class QueryRouteEnhancer implements EnhancerInterface {
14
15
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
+
15
30
/**
16
31
* {@inheritdoc}
17
32
*/
@@ -21,6 +36,10 @@ public function enhance(array $defaults, Request $request) {
21
36
return $ defaults ;
22
37
}
23
38
39
+ if ($ request ->getMethod () === "POST " ) {
40
+ $ this ->assertValidPostRequestHeaders ($ request );
41
+ }
42
+
24
43
$ helper = new Helper ();
25
44
$ method = $ request ->getMethod ();
26
45
$ body = $ this ->extractBody ($ request );
@@ -37,8 +56,89 @@ public function enhance(array $defaults, Request $request) {
37
56
}
38
57
39
58
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. " );
42
142
}
43
143
44
144
/**
0 commit comments