Skip to content

Commit

Permalink
Merge branch '3.8' into 3
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Oct 16, 2023
2 parents 2badd3e + 7638a3f commit 4c1da0c
Show file tree
Hide file tree
Showing 7 changed files with 509 additions and 9 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:
# Every Wednesday at 11:00am UTC
schedule:
- cron: '0 11 * * 3'

jobs:
ci:
name: CI
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && startsWith(github.repository, 'silverstripe/')) || (github.event_name != 'schedule')
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
with:
extra_jobs: |
- php: 7.4
endtoend: true
endtoend_suite: asset-admin
endtoend_config: vendor/silverstripe/asset-admin/behat.yml
17 changes: 17 additions & 0 deletions .github/workflows/keepalive.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Keepalive

on:
workflow_dispatch:
# The 4th of every month at 10:50am UTC
schedule:
- cron: '50 10 4 * *'

jobs:
keepalive:
name: Keepalive
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && startsWith(github.repository, 'silverstripe/')) || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Keepalive
uses: silverstripe/gha-keepalive@v1
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Silverstripe GraphQL Server

[![Build Status](https://api.travis-ci.com/silverstripe/silverstripe-graphql.svg?branch=3)](https://travis-ci.com/silverstripe/silverstripe-graphql)
[![CI](https://github.com/silverstripe/silverstripe-graphql/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-graphql/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/silverstripe/silverstripe-graphql/branch/master/graph/badge.svg)](https://codecov.io/gh/silverstripe/silverstripe-graphql)
[![SilverStripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)

Expand Down Expand Up @@ -71,7 +71,8 @@ composer require silverstripe/graphql
- [CSRF tokens (required for mutations)](#csrf-tokens-required-for-mutations)
- [Cross-Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors)
- [Sample Custom CORS Config](#sample-custom-cors-config)
- [Persisting Queries](#persisting-queries)
- [Recursive or complex queries](#recursive-or-complex-queries)
- [Persisting Queries](#persisting-queries)
- [Schema introspection](#schema-introspection)
- [Setting up a new GraphQL schema](#setting-up-a-new-graphql-schema)
- [Strict HTTP Method Checking](#strict-http-method-checking)
Expand Down Expand Up @@ -2391,8 +2392,42 @@ SilverStripe\Core\Injector\Injector:
properties:
corsConfig:
Enabled: false
```
```

## Recursive or complex queries

GraphQL schemas can contain recursive types and circular dependencies. Recursive or overly complex queries can take up a lot of resources,
and could have a high impact on server performance and even result in a denial of service if not handled carefully.

Before parsing queries, if a query is found to have more than 500 nodes, it is rejected.

While executing queries, there is a default query depth limit of 15 for all schemas, and no current complexity limit.

For calculating the query complexity, every field in the query gets a default score 1 (including ObjectType nodes). Total complexity of the query is the sum of all field scores.

You can customise the node limit and query depth and complexity limits by setting the following configuration:

**app/_config/graphql.yml**

```yaml
SilverStripe\GraphQL\Manager:
default_max_query_nodes: 250 # default 500
default_max_query_depth: 20 # default 15
default_max_query_complexity: 100 # default unlimited
```

You can also configure these settings for individual schemas. This allows you to fine-tune the security of your custom public-facing schema without affecting the security of the schema used in the CMS. To do so, set the values for your schema like so:

**app/_config/graphql.yml**

```yaml
SilverStripe\GraphQL\Manager:
schemas:
default:
max_query_nodes: 250
max_query_depth: 20
max_query_complexity: 100
```

## Persisting queries

Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
"dnadesign/silverstripe-elemental": "<4.6"
},
"require-dev": {
"silverstripe/asset-admin": "^1.10",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.0"
"squizlabs/php_codesniffer": "^3.0",
"silverstripe/frameworktest": "^0.4.3"
},
"autoload": {
"psr-4": {
Expand Down
116 changes: 115 additions & 1 deletion src/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
use SilverStripe\Security\Security;
use BadMethodCallException;
use Exception;
use GraphQL\Language\Lexer;
use GraphQL\Language\Source;
use GraphQL\Language\Token;
use GraphQL\Utils\Utils;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;

/**
* Manager is the master container for a graphql endpoint, and contains
Expand All @@ -47,6 +54,36 @@ class Manager implements ConfigurationApplier

const TYPES_ROOT = 'types';

/**
* Default maximum query nodes if not defined in the current schema config
*/
private static $default_max_query_nodes = 500;

/**
* Default maximum query depth if not defined in the current schema config
*/
private static $default_max_query_depth = 15;

/**
* Default maximum query complexity if not defined in the current schema config
*/
private static $default_max_query_complexity = 0;

/**
* Maximum query nodes allowed for the current schema config
*/
private ?int $maxQueryNodes = null;

/**
* Maximum query depth allowed for the current schema config
*/
private ?int $maxQueryDepth = null;

/**
* Maximum query complexity allowed for the current schema config
*/
private ?int $maxQueryComplexity = null;

/**
* @var string
*/
Expand Down Expand Up @@ -204,6 +241,17 @@ public function applyConfig(array $config)
{
$this->extend('updateConfig', $config);

// Security validation rules
if (array_key_exists('max_query_nodes', $config)) {
$this->maxQueryNodes = $config['max_query_nodes'];
}
if (array_key_exists('max_query_depth', $config)) {
$this->maxQueryDepth = $config['max_query_depth'];
}
if (array_key_exists('max_query_complexity', $config)) {
$this->maxQueryComplexity = $config['max_query_complexity'];
}

// Bootstrap schema class mapping from config
if (array_key_exists('typeNames', $config ?? [])) {
StaticSchema::inst()->setTypeNames($config['typeNames']);
Expand Down Expand Up @@ -372,12 +420,78 @@ public function queryAndReturnResult($query, $params = [])
$context = $this->getContext();

$last = function ($schema, $query, $context, $params) {
return GraphQL::executeQuery($schema, $query, null, $context, $params);
if (is_string($query)) {
$this->validateQueryBeforeParsing($query, $context);
}

$validationRules = DocumentValidator::allRules();
$maxDepth = $this->getMaxQueryDepth();
$maxComplexity = $this->getMaxQueryComplexity();
if ($maxDepth) {
$validationRules[QueryDepth::class] = new QueryDepth($maxDepth);
}
if ($maxComplexity) {
$validationRules[QueryComplexity::class] = new QueryComplexity($maxComplexity);
}
return GraphQL::executeQuery($schema, $query, null, $context, $params, null, null, $validationRules);
};

return $this->callMiddleware($schema, $query, $context, $params, $last);
}

/**
* Validate a query before parsing it in case there are issues we can catch early.
*/
private function validateQueryBeforeParsing(string $query): void
{
$maxNodes = $this->getMaxQueryNodes();

if (!$maxNodes) {
return;
}

$lexer = new Lexer(new Source($query));
$numNodes = 0;

// Check how many nodes there are in this query
do {
$next = $lexer->advance();
if ($next->kind === Token::NAME) {
$numNodes++;
}
} while ($next->kind !== Token::EOF && $numNodes <= $maxNodes);

// Throw a GraphQL Invariant violation if there are too many nodes
Utils::invariant(
$maxNodes >= $numNodes,
"GraphQL query body must not be longer than $maxNodes nodes."
);
}

private function getMaxQueryNodes()
{
if ($this->maxQueryNodes !== null) {
return $this->maxQueryNodes;
}
return static::config()->get('default_max_query_nodes');
}

private function getMaxQueryDepth()
{
if ($this->maxQueryDepth !== null) {
return $this->maxQueryDepth;
}
return static::config()->get('default_max_query_depth');
}

private function getMaxQueryComplexity()
{
if ($this->maxQueryComplexity !== null) {
return $this->maxQueryComplexity;
}
return static::config()->get('default_max_query_complexity');
}

/**
* Register a new type
*
Expand Down
Loading

0 comments on commit 4c1da0c

Please sign in to comment.