Skip to content

Commit 3c1dd6b

Browse files
author
Aaron Carlino
committed
[CVE-2019-12437] Cross Site Request Forgery (CSRF) Protection Bypass
1 parent d366543 commit 3c1dd6b

File tree

3 files changed

+89
-2
lines changed

3 files changed

+89
-2
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,9 @@ resources are accessed.
19581958
The `MemberAuthenticator` class is configured as the default option for authentication,
19591959
and will attempt to use the current CMS `Member` session for authentication context.
19601960

1961+
**If you are using the default session-based authentication, please be sure that you have
1962+
the [CSRF Middleware](#csrf-tokens-required-for-mutations) enabled. (It is by default).**
1963+
19611964
### HTTP basic authentication
19621965

19631966
Silverstripe has built in support for [HTTP basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
@@ -1967,6 +1970,9 @@ authenticator because GraphQL needs to use the successfully authenticated member
19671970
for CMS permission filtering, whereas the global `BasicAuth` does not log the
19681971
member in or use it for model security.
19691972

1973+
When using HTTP basic authentication, you can feel free to remove the [CSRF Middleware](#csrf-tokens-required-for-mutations),
1974+
as it just adds unnecessary overhead to the request.
1975+
19701976
#### In GraphiQL
19711977

19721978
If you want to add basic authentication support to your GraphQL requests you can
@@ -2033,12 +2039,19 @@ the `SecurityToken` API, using `SecurityToken::inst()->getValue()`.
20332039

20342040
Queries do not require CSRF tokens.
20352041

2042+
### Disabling CSRF protection (for token-based authentication only)
2043+
2044+
If you are using HTTP basic authentication or a token-based system like OAuth or [JWT](https://github.com/Firesphere/silverstripe-graphql-jwt),
2045+
you will want to remove the CSRF protection, as it just adds unnecessary overhead. You can do this by setting
2046+
the middleware to `false`.
2047+
20362048
```yaml
2037-
SilverStripe\GraphQL\Manager:
2049+
SilverStripe\GraphQL\Manager.default:
20382050
properties:
20392051
Middlewares:
20402052
CSRFMiddleware: false
20412053
```
2054+
20422055
## Cross-Origin Resource Sharing (CORS)
20432056

20442057
By default [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) is disabled in the GraphQL Server. This can be easily enabled via YAML:

src/Middleware/CSRFMiddleware.php

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
namespace SilverStripe\GraphQL\Middleware;
44

55
use GraphQL\Schema;
6+
use GraphQL\Language\Parser;
7+
use GraphQL\Language\Source;
8+
use GraphQL\Language\AST\NodeKind;
9+
use SilverStripe\GraphQL\Manager;
610
use SilverStripe\GraphQL\Middleware\QueryMiddleware;
711
use SilverStripe\Security\SecurityToken;
812
use Exception;
@@ -11,7 +15,7 @@ class CSRFMiddleware implements QueryMiddleware
1115
{
1216
public function process(Schema $schema, $query, $context, $params, callable $next)
1317
{
14-
if (preg_match('/^\s*mutation/', $query)) {
18+
if ($this->isMutation($query)) {
1519
if (empty($context['token'])) {
1620
throw new Exception('Mutations must provide a CSRF token in the X-CSRF-TOKEN header');
1721
}
@@ -24,4 +28,39 @@ public function process(Schema $schema, $query, $context, $params, callable $nex
2428

2529
return $next($schema, $query, $context, $params);
2630
}
31+
32+
/**
33+
* @param string $query
34+
* @return bool
35+
*/
36+
protected function isMutation($query)
37+
{
38+
// Simple string matching as a first check to prevent unnecessary static analysis
39+
if (stristr($query, Manager::MUTATION_ROOT) === false) {
40+
return false;
41+
}
42+
43+
// If "mutation" is the first expression in the query, then it's a mutation.
44+
if (preg_match('/^\s*'.preg_quote(Manager::MUTATION_ROOT, '/').'/', $query)) {
45+
return true;
46+
}
47+
48+
// Otherwise, bring in the big guns.
49+
$document = Parser::parse(new Source($query ?: 'GraphQL'));
50+
$defs = $document->definitions;
51+
foreach ($defs as $statement) {
52+
$options = [
53+
NodeKind::OPERATION_DEFINITION,
54+
NodeKind::OPERATION_TYPE_DEFINITION
55+
];
56+
if (!in_array($statement->kind, $options, true)) {
57+
continue;
58+
}
59+
if ($statement->operation === Manager::MUTATION_ROOT) {
60+
return true;
61+
}
62+
}
63+
64+
return false;
65+
}
2766
}

tests/Middleware/CSRFMiddlewareTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,42 @@ public function testItThrowsIfNoTokenIsProvided()
3030
' mutation someMutation { tester }'
3131
);
3232
$this->assertNotEquals('resolved', $result);
33+
$result = $this->simulateMiddlewareProcess(
34+
new CSRFMiddleware(),
35+
' mutation someMutation { tester }'
36+
);
37+
$this->assertNotEquals('resolved', $result);
38+
$graphql = <<<GRAPHQL
39+
mutation MyMutation(\$SomeArg:string!) {
40+
someMutation(Foo:\$SomeArg) {
41+
tester
3342
}
43+
}
44+
GRAPHQL;
45+
46+
$result = $this->simulateMiddlewareProcess(
47+
new CSRFMiddleware(),
48+
$graphql
49+
);
50+
$this->assertNotEquals('resolved', $result);
51+
$graphql = <<<GRAPHQL
52+
fragment myFragment on File {
53+
id
54+
width
55+
}
56+
mutation someMutation {
57+
tester
58+
}
59+
}
60+
GRAPHQL;
61+
62+
$result = $this->simulateMiddlewareProcess(
63+
new CSRFMiddleware(),
64+
$graphql
65+
);
66+
$this->assertNotEquals('resolved', $result);
67+
}
68+
3469

3570
public function testItThrowsIfTokenIsInvalid()
3671
{

0 commit comments

Comments
 (0)