diff --git a/README.md b/README.md index 9036037..a1cc75e 100644 --- a/README.md +++ b/README.md @@ -559,6 +559,24 @@ $queryDepth->setMaxQueryDepth($maxQueryDepth = 10); GraphQL::execute(/*...*/); ``` +#### Disabling Introspection + +This is a PHP port of [graphql-disable-introspection](https://github.com/helfer/graphql-disable-introspection). +The rule prohibits queries that contain `__type` or `__schema` fields. + +This document validator rule is disabled by default. Here an example to enable it: + +```php +use GraphQL\GraphQL; +use GraphQL\Validator\Rules\DisableIntrospection; + +/** @var \GraphQL\Validator\Rules\DisableIntrospection $disableIntrospection */ +$disableIntrospection = DocumentValidator::getRule('DisableIntrospection'); +$disableIntrospection->setEnabled(DisableIntrospection::ENABLED); + +GraphQL::execute(/*...*/); +``` + ### More Examples Make sure to check [tests](https://github.com/webonyx/graphql-php/tree/master/tests) for more usage examples. diff --git a/src/GraphQL.php b/src/GraphQL.php index d0d3ec6..1572c82 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -42,9 +42,9 @@ class GraphQL /** * @param Schema $schema * @param string|DocumentNode $requestString - * @param null $rootValue - * @param null $variableValues - * @param null $operationName + * @param mixed $rootValue + * @param array|null $variableValues + * @param string|null $operationName * @return ExecutionResult|Promise */ public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 540765d..837080a 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -25,6 +25,7 @@ use GraphQL\Utils; use GraphQL\Utils\TypeInfo; use GraphQL\Validator\Rules\ArgumentsOfCorrectType; use GraphQL\Validator\Rules\DefaultValuesOfCorrectType; +use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\FieldsOnCorrectType; use GraphQL\Validator\Rules\FragmentsOnCompositeTypes; use GraphQL\Validator\Rules\KnownArgumentNames; @@ -100,6 +101,7 @@ class DocumentValidator 'UniqueInputFieldNames' => new UniqueInputFieldNames(), // Query Security + 'DisableIntrospection' => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED 'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled 'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled ]; diff --git a/src/Validator/Rules/DisableIntrospection.php b/src/Validator/Rules/DisableIntrospection.php new file mode 100644 index 0000000..f820375 --- /dev/null +++ b/src/Validator/Rules/DisableIntrospection.php @@ -0,0 +1,50 @@ +setEnabled($enabled); + } + + public function setEnabled($enabled) + { + $this->isEnabled = $enabled; + } + + static function introspectionDisabledMessage() + { + return 'GraphQL introspection is not allowed, but the query contained __schema or __type'; + } + + protected function isEnabled() + { + return $this->isEnabled !== static::DISABLED; + } + + public function __invoke(ValidationContext $context) + { + return $this->invokeIfNeeded( + $context, + [ + NodeKind::FIELD => function (FieldNode $node) use ($context) { + if ($node->name->value === '__type' || $node->name->value === '__schema') { + $context->reportError(new Error( + static::introspectionDisabledMessage(), + [$node] + )); + } + } + ] + ); + } +} diff --git a/tests/Validator/DisableIntrospectionTest.php b/tests/Validator/DisableIntrospectionTest.php new file mode 100644 index 0000000..88d771c --- /dev/null +++ b/tests/Validator/DisableIntrospectionTest.php @@ -0,0 +1,128 @@ +expectFailsRule(new DisableIntrospection(DisableIntrospection::ENABLED), ' + query { + __schema { + queryType { + name + } + } + } + ', + [$this->error(3, 9)] + ); + } + + /** + * @it fails if the query contains __type + */ + public function testQueryContainsType() + { + $this->expectFailsRule(new DisableIntrospection(DisableIntrospection::ENABLED), ' + query { + __type( + name: "Query" + ){ + name + } + } + ', + [$this->error(3, 9)] + ); + } + + /** + * @it does not fail on a query that does not contain __type + */ + public function testValidQuery() + { + $this->expectPassesRule(new DisableIntrospection(DisableIntrospection::ENABLED), ' + query { + user { + name + email + friends { + name + } + } + } + '); + } + + /** + * @it does not fail when not enabled + */ + public function testQueryWhenDisabled() + { + $this->expectPassesRule(new DisableIntrospection(DisableIntrospection::DISABLED), ' + query { + __type( + name: "Query" + ){ + name + } + } + '); + } + + /** + * @it has a public interface for enabeling the rule + */ + public function testPublicEnableInterface() + { + $disableIntrospection = new DisableIntrospection(DisableIntrospection::DISABLED); + $disableIntrospection->setEnabled(DisableIntrospection::ENABLED); + $this->expectFailsRule($disableIntrospection, ' + query { + __type( + name: "Query" + ){ + name + } + } + ', + [$this->error(3, 9)] + ); + } + + /** + * @it has a public interface for disableing the rule + */ + public function testPublicDisableInterface() + { + $disableIntrospection = new DisableIntrospection(DisableIntrospection::ENABLED); + $disableIntrospection->setEnabled(DisableIntrospection::DISABLED); + $this->expectPassesRule($disableIntrospection, ' + query { + __type( + name: "Query" + ){ + name + } + } + '); + } + + + private function error($line, $column) + { + return FormattedError::create( + DisableIntrospection::introspectionDisabledMessage(), + [ new SourceLocation($line, $column) ] + ); + } +}