Skip to content

Commit

Permalink
fix(routing): Fix handling of POST requests
Browse files Browse the repository at this point in the history
  • Loading branch information
klausi committed Nov 8, 2023
1 parent b585ae1 commit 3aaa111
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 4 deletions.
2 changes: 1 addition & 1 deletion graphql.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ services:
# Upcasting for graphql query request parameters.
graphql.route_enhancer.query:
class: Drupal\graphql\Routing\QueryRouteEnhancer
arguments: ['@graphql.query_provider']
arguments: ['%cors.config%']
tags:
- { name: route_enhancer }

Expand Down
106 changes: 103 additions & 3 deletions src/Routing/QueryRouteEnhancer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@

namespace Drupal\graphql\Routing;

use Asm89\Stack\CorsService;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Routing\EnhancerInterface;
use Drupal\Core\Routing\RouteObjectInterface;
use Drupal\graphql\Utility\JsonHelper;
use GraphQL\Server\Helper;
use Symfony\Component\HttpFoundation\Request;

use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class QueryRouteEnhancer implements EnhancerInterface {

/**
* The CORS options for Origin header checking.
*
* @var array
*/
protected $corsOptions;

/**
* Constructor.
*/
public function __construct(array $corsOptions) {
$this->corsOptions = $corsOptions;
}

/**
* {@inheritdoc}
*/
Expand All @@ -21,6 +36,10 @@ public function enhance(array $defaults, Request $request) {
return $defaults;
}

if ($request->getMethod() === "POST") {
$this->assertValidPostRequestHeaders($request);
}

$helper = new Helper();
$method = $request->getMethod();
$body = $this->extractBody($request);
Expand All @@ -37,8 +56,89 @@ public function enhance(array $defaults, Request $request) {
}

return $defaults + [
'operations' => $operations,
];
'operations' => $operations,
];
}

/**
* Ensures that the headers for a POST request have triggered a preflight.
*
* POST requests must be submitted with content-type headers that properly
* trigger a cross-origin preflight request. In case content-headers are used
* that would trigger a "simple" request then custom headers must be provided.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request to check.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* In case the headers indicated a preflight was not performed.
*/
protected function assertValidPostRequestHeaders(Request $request): void {
$content_type = $request->headers->get('content-type');
if ($content_type === NULL) {
throw new BadRequestHttpException("GraphQL requests must specify a valid content type header.");
}

// application/graphql is a non-standard header that's supported by our
// server implementation and triggers CORS.
if ($content_type === "application/graphql") {
return;
}

/** @phpstan-ignore-next-line */
$content_format = method_exists($request, 'getContentTypeFormat') ? $request->getContentTypeFormat() : $request->getContentType();
if ($content_format === NULL) {
// Symfony before 5.4 does not detect "multipart/form-data", check for it
// manually.
if (stripos($content_type, 'multipart/form-data') === 0) {
$content_format = 'form';
}
else {
throw new BadRequestHttpException("The content type '$content_type' is not supported.");
}
}

// JSON requests provide a non-standard header that trigger CORS.
if ($content_format === "json") {
return;
}

// The form content types are considered simple requests and don't trigger
// CORS pre-flight checks, so these require a separate header to prevent
// CSRF. We need to support "form" for file uploads.
if ($content_format === "form") {
// If the client set a custom header then we can be sure CORS was
// respected.
$custom_headers = [
'Apollo-Require-Preflight',
'X-Apollo-Operation-Name',
'x-graphql-yoga-csrf',
];
foreach ($custom_headers as $custom_header) {
if ($request->headers->has($custom_header)) {
return;
}
}
// 1. Allow requests that have set no Origin header at all, for example
// server-to-server requests.
// 2. Allow requests where the Origin matches the site's domain name.
$origin = $request->headers->get('Origin');
if ($origin === NULL || $request->getSchemeAndHttpHost() === $origin) {
return;
}
// Allow other origins as configured in the CORS policy.
if (!empty($this->corsOptions['enabled'])) {
$cors_service = new CorsService($this->corsOptions);
// Drupal 9 compatibility, method name has changed in Drupal 10.
/** @phpstan-ignore-next-line */
if ($cors_service->isActualRequestAllowed($request)) {
return;
}
}
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.");
}

throw new BadRequestHttpException("The content type '$content_type' is not supported.");
}

/**
Expand Down

0 comments on commit 3aaa111

Please sign in to comment.