Skip to content

Commit

Permalink
Merge pull request #32 from mcg-web/add_query_security_rules
Browse files Browse the repository at this point in the history
Add query security document validation rules
  • Loading branch information
vladar committed Apr 15, 2016
2 parents 68d8681 + 545fe61 commit 36a8454
Show file tree
Hide file tree
Showing 56 changed files with 1,211 additions and 95 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,42 @@ header('Content-Type: application/json');
echo json_encode($result);
```

### Security

#### Query Complexity Analysis

This is a PHP port of [Query Complexity Analysis](http://sangria-graphql.org/learn/#query-complexity-analysis) in Sangria implementation.
Introspection query with description max complexity is **109**.

This document validator rule is disabled by default. Here an example to enabled it:

```php
use GraphQL\GraphQL;

/** @var \GraphQL\Validator\Rules\QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
$queryComplexity->setMaxQueryComplexity($maxQueryComplexity = 110);

GraphQL::execute(/*...*/);
```

#### Limiting Query Depth

This is a PHP port of [Limiting Query Depth](http://sangria-graphql.org/learn/#limiting-query-depth) in Sangria implementation.
Introspection query with description max depth is **7**.

This document validator rule is disabled by default. Here an example to enabled it:

```php
use GraphQL\GraphQL;

/** @var \GraphQL\Validator\Rules\QueryDepth $queryDepth */
$queryDepth = DocumentValidator::getRule('QueryDepth');
$queryDepth->setMaxQueryDepth($maxQueryDepth = 10);

GraphQL::execute(/*...*/);
```

### More Examples
Make sure to check [tests](https://github.com/webonyx/graphql-php/tree/master/tests) for more usage examples.

Expand Down
14 changes: 6 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@
"bin-dir": "bin"
},
"autoload": {
"classmap": [
"src/"
]
"psr-4": {
"GraphQL\\": "src/"
}
},
"autoload-dev": {
"classmap": [
"tests/"
],
"files": [
]
"psr-4": {
"GraphQL\\Tests\\": "tests/"
}
}
}
34 changes: 34 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>

<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="webonyx/graphql-php Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>

<php>
<ini name="error_reporting" value="E_ALL"/>
</php>

</phpunit>
6 changes: 6 additions & 0 deletions src/GraphQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;

class GraphQL
{
Expand Down Expand Up @@ -35,6 +36,11 @@ public static function executeAndReturnResult(Schema $schema, $requestString, $r
try {
$source = new Source($requestString ?: '', 'GraphQL request');
$documentAST = Parser::parse($source);

/** @var QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
$queryComplexity->setRawVariableValues($variableValues);

$validationErrors = DocumentValidator::validate($schema, $documentAST);

if (!empty($validationErrors)) {
Expand Down
18 changes: 18 additions & 0 deletions src/Type/Definition/FieldDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

class FieldDefinition
{
const DEFAULT_COMPLEXITY_FN = 'GraphQL\Type\Definition\FieldDefinition::defaultComplexity';

/**
* @var string
*/
Expand Down Expand Up @@ -72,6 +74,7 @@ public static function getDefinition()
'map' => Config::CALLBACK,
'description' => Config::STRING,
'deprecationReason' => Config::STRING,
'complexity' => Config::CALLBACK,
]);
}

Expand Down Expand Up @@ -113,6 +116,8 @@ protected function __construct(array $config)
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;

$this->config = $config;

$this->complexityFn = isset($config['complexity']) ? $config['complexity'] : static::DEFAULT_COMPLEXITY_FN;
}

/**
Expand Down Expand Up @@ -141,4 +146,17 @@ public function getType()
}
return $this->resolvedType;
}

/**
* @return callable|\Closure
*/
public function getComplexityFn()
{
return $this->complexityFn;
}

public static function defaultComplexity($childrenComplexity)
{
return $childrenComplexity + 1;
}
}
108 changes: 70 additions & 38 deletions src/Validator/DocumentValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,59 +34,91 @@
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesInAllowedPosition;

class DocumentValidator
{
private static $allRules;
private static $rules = [];

static function allRules()
private static $defaultRules;

private static $initRules = false;

public static function allRules()
{
if (!self::$initRules) {
self::$rules = array_merge(static::defaultRules(), self::$rules);
self::$initRules = true;
}

return self::$rules;
}

public static function defaultRules()
{
if (null === self::$allRules) {
self::$allRules = [
if (null === self::$defaultRules) {
self::$defaultRules = [
// new UniqueOperationNames,
// new LoneAnonymousOperation,
new KnownTypeNames,
new FragmentsOnCompositeTypes,
new VariablesAreInputTypes,
new ScalarLeafs,
new FieldsOnCorrectType,
'KnownTypeNames' => new KnownTypeNames(),
'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(),
'VariablesAreInputTypes' => new VariablesAreInputTypes(),
'ScalarLeafs' => new ScalarLeafs(),
'FieldsOnCorrectType' => new FieldsOnCorrectType(),
// new UniqueFragmentNames,
new KnownFragmentNames,
new NoUnusedFragments,
new PossibleFragmentSpreads,
new NoFragmentCycles,
new NoUndefinedVariables,
new NoUnusedVariables,
new KnownDirectives,
new KnownArgumentNames,
'KnownFragmentNames' => new KnownFragmentNames(),
'NoUnusedFragments' => new NoUnusedFragments(),
'PossibleFragmentSpreads' => new PossibleFragmentSpreads(),
'NoFragmentCycles' => new NoFragmentCycles(),
'NoUndefinedVariables' => new NoUndefinedVariables(),
'NoUnusedVariables' => new NoUnusedVariables(),
'KnownDirectives' => new KnownDirectives(),
'KnownArgumentNames' => new KnownArgumentNames(),
// new UniqueArgumentNames,
new ArgumentsOfCorrectType,
new ProvidedNonNullArguments,
new DefaultValuesOfCorrectType,
new VariablesInAllowedPosition,
new OverlappingFieldsCanBeMerged,
'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(),
'ProvidedNonNullArguments' => new ProvidedNonNullArguments(),
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
// Query Security
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
];
}
return self::$allRules;

return self::$defaultRules;
}

public static function getRule($name)
{
$rules = static::allRules();

return isset($rules[$name]) ? $rules[$name] : null ;
}

public static function addRule($name, callable $rule)
{
self::$rules[$name] = $rule;
}

public static function validate(Schema $schema, Document $ast, array $rules = null)
{
$errors = self::visitUsingRules($schema, $ast, $rules ?: self::allRules());
$errors = static::visitUsingRules($schema, $ast, $rules ?: static::allRules());
return $errors;
}

static function isError($value)
public static function isError($value)
{
return is_array($value)
? count(array_filter($value, function($item) { return $item instanceof \Exception;})) === count($value)
: $value instanceof \Exception;
}

static function append(&$arr, $items)
public static function append(&$arr, $items)
{
if (is_array($items)) {
$arr = array_merge($arr, $items);
Expand All @@ -96,7 +128,7 @@ static function append(&$arr, $items)
return $arr;
}

static function isValidLiteralValue($valueAST, Type $type)
public static function isValidLiteralValue($valueAST, Type $type)
{
// A value can only be not provided if the type is nullable.
if (!$valueAST) {
Expand All @@ -105,7 +137,7 @@ static function isValidLiteralValue($valueAST, Type $type)

// Unwrap non-null.
if ($type instanceof NonNull) {
return self::isValidLiteralValue($valueAST, $type->getWrappedType());
return static::isValidLiteralValue($valueAST, $type->getWrappedType());
}

// This function only tests literals, and assumes variables will provide
Expand All @@ -123,13 +155,13 @@ static function isValidLiteralValue($valueAST, Type $type)
$itemType = $type->getWrappedType();
if ($valueAST instanceof ListValue) {
foreach($valueAST->values as $itemAST) {
if (!self::isValidLiteralValue($itemAST, $itemType)) {
if (!static::isValidLiteralValue($itemAST, $itemType)) {
return false;
}
}
return true;
} else {
return self::isValidLiteralValue($valueAST, $itemType);
return static::isValidLiteralValue($valueAST, $itemType);
}
}

Expand Down Expand Up @@ -157,7 +189,7 @@ static function isValidLiteralValue($valueAST, Type $type)
}
}
foreach ($fieldASTs as $fieldAST) {
if (empty($fields[$fieldAST->name->value]) || !self::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) {
if (empty($fields[$fieldAST->name->value]) || !static::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) {
return false;
}
}
Expand Down Expand Up @@ -231,8 +263,8 @@ public static function visitUsingRules(Schema $schema, Document $documentAST, ar
} else if ($result->doBreak) {
$instances[$i] = null;
}
} else if ($result && self::isError($result)) {
self::append($errors, $result);
} else if ($result && static::isError($result)) {
static::append($errors, $result);
for ($j = $i - 1; $j >= 0; $j--) {
$leaveFn = Visitor::getVisitFn($instances[$j], true, $node->kind);
if ($leaveFn) {
Expand All @@ -243,8 +275,8 @@ public static function visitUsingRules(Schema $schema, Document $documentAST, ar
if ($result->doBreak) {
$instances[$j] = null;
}
} else if (self::isError($result)) {
self::append($errors, $result);
} else if (static::isError($result)) {
static::append($errors, $result);
} else if ($result !== null) {
throw new \Exception("Config cannot edit document.");
}
Expand Down Expand Up @@ -294,8 +326,8 @@ public static function visitUsingRules(Schema $schema, Document $documentAST, ar
if ($result->doBreak) {
$instances[$i] = null;
}
} else if (self::isError($result)) {
self::append($errors, $result);
} else if (static::isError($result)) {
static::append($errors, $result);
} else if ($result !== null) {
throw new \Exception("Config cannot edit document.");
}
Expand All @@ -309,7 +341,7 @@ public static function visitUsingRules(Schema $schema, Document $documentAST, ar
// Visit the whole document with instances of all provided rules.
$allRuleInstances = [];
foreach ($rules as $rule) {
$allRuleInstances[] = $rule($context);
$allRuleInstances[] = call_user_func_array($rule, [$context]);
}
$visitInstances($documentAST, $allRuleInstances);

Expand Down
Loading

0 comments on commit 36a8454

Please sign in to comment.