Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW: Disable schema introspection in non-dev #400

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 53 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ 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)
- [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 @@ -165,7 +165,7 @@ through YAML configuration:
SilverStripe\GraphQL\Manager:
schemas:
default:
types:
types:
member: 'MyProject\GraphQL\MemberTypeCreator'
```

Expand Down Expand Up @@ -547,7 +547,7 @@ in your constructor.

```php
$this->queryFilter = DataObjectQueryFilter::create(MyDataObject::class);
```
```

You can then add filters to fields of the dataobject.

Expand All @@ -557,7 +557,7 @@ $this->queryFilter
->addFieldFilterByIdentifier('CommentCount', 'gt')
->addFieldFilterByIdentifier('Categories__Title', 'in')
->addFieldFilterByIdentifier('Hidden', 'eq');
```
```

Don't worry about the filter keys (`contains`, `gt`, `eq`, etc) for now. That will be explained [further down](#the-filter-registry).

Expand All @@ -584,11 +584,11 @@ public function resolve($obj, $args = [], $context = [], ResolveInfo $info)
{
$list = MyDataObject::get();
$list = $this->queryFilter->applyArgsToList($list, $args);

return $list;
}
```

#### Shortcuts (for the 80% case)

All `SilverStripe\ORM\DBField` instances are configured to have a set of "default" filters (see `filters.yml`).
Expand Down Expand Up @@ -666,7 +666,7 @@ class MyCustomFieldFilter implements FieldFilterInterface
{
return 'eq';
}

public function applyInclusion(DataList $list, $fieldName, $value)
{
return $list->addWhere([
Expand Down Expand Up @@ -987,7 +987,7 @@ You can add all default filters for every field on your dataobject with `filters
```yaml
read:
filters: '*'
```
```
> Note: "every field" means every field exposed by `searchable_fields` on the dataobject -- not just those exposed on its GraphQL type.

To be more granular, break it up into a list of specific fields.
Expand Down Expand Up @@ -2239,7 +2239,7 @@ session.
In the absence of a token-based authentication system, like OAuth, the best countermeasure to this
is the use of a CSRF token for any requests that destroy or mutate data.

By default, this module comes with a `CSRFMiddleware` implementation that forces all mutations to check
By default, this module comes with a `CSRFMiddleware` implementation that forces all mutations to check
for the presence of a CSRF token in the request. That token must be applied to a header named` X-CSRF-TOKEN`.

In SilverStripe, CSRF tokens are most commonly stored in the session as `SecurityID`, or accessed through
Expand Down Expand Up @@ -2347,27 +2347,27 @@ Once you have enabled CORS you can then control four new headers in the HTTP Res
```yaml
Max-Age: 600
```

5. **Access-Control-Allow-Credentials.**

When a request's credentials mode (Request.credentials) is "include", browsers
will only expose the response to frontend JavaScript code if the
will only expose the response to frontend JavaScript code if the
Access-Control-Allow-Credentials value is true.

The Access-Control-Allow-Credentials header works in conjunction with the
XMLHttpRequest.withCredentials property or with the credentials option in the
Request() constructor of the Fetch API. For a CORS request with credentials,
Request() constructor of the Fetch API. For a CORS request with credentials,
in order for browsers to expose the response to frontend JavaScript code, both
the server (using the Access-Control-Allow-Credentials header) and the client
(by setting the credentials mode for the XHR, Fetch, or Ajax request) must
the server (using the Access-Control-Allow-Credentials header) and the client
(by setting the credentials mode for the XHR, Fetch, or Ajax request) must
indicate that they’re opting in to including credentials.

This is set to empty by default but can be changed in YAML as in this example:

```yaml
Allow-Credentials: 'true'
```

### Apply a CORS config to all GraphQL endpoints

```yaml
Expand All @@ -2380,7 +2380,7 @@ SilverStripe\GraphQL\Controller:
Allow-Methods: 'GET, POST, OPTIONS'
Allow-Credentials: 'true'
Max-Age: 600 # 600 seconds = 10 minutes.
```
```

### Apply a CORS config to a single GraphQL endpoint

Expand All @@ -2391,7 +2391,7 @@ SilverStripe\Core\Injector\Injector:
properties:
corsConfig:
Enabled: false
```
```


## Persisting queries
Expand All @@ -2411,7 +2411,7 @@ which cover most use cases:

### Configuring query mapping providers

All of these implementations can be configured through `Injector`. Note that each schema gets its
All of these implementations can be configured through `Injector`. Note that each schema gets its
own set of persisted queries. In these examples, we're using the `default`schema.

#### FileProvider
Expand Down Expand Up @@ -2470,18 +2470,18 @@ To access a persisted query, simply pass an `id` parameter in the request in lie
Note that if you pass `query` along with `id`, an exception will be thrown.

## Schema introspection
Some GraphQL clients such as [Apollo](http://apollographql.com) require some level of introspection
into the schema. While introspection is [part of the GraphQL spec](http://graphql.org/learn/introspection/),
Some GraphQL clients such as [Apollo](http://apollographql.com) require some level of introspection
into the schema. While introspection is [part of the GraphQL spec](http://graphql.org/learn/introspection/),
this module provides a limited API for fetching it via non-graphql endpoints. By default, the `graphql/`
controller provides a `types` action that will return the type schema (serialised as JSON) dynamically.

*GET http://example.com/graphql/types*
```js
{
"data":{
"__schema":{
"types":[
{
{
"data":{
"__schema":{
"types":[
{
"kind":"OBJECT",
"name":"Query",
"possibleTypes":null
Expand All @@ -2493,7 +2493,7 @@ controller provides a `types` action that will return the type schema (serialise

```

As your schema grows, introspecting it dynamically may have a performance hit. Alternatively,
As your schema grows, introspecting it dynamically may have a performance hit. Alternatively,
if you have the `silverstripe/assets` module installed (as it is in the default SilverStripe installation),
GraphQL can cache your schema as a flat file in the `assets/` directory. To enable this, simply
set the `cache_types_in_filesystem` setting to `true` on `SilverStripe\GraphQL\Controller`. Once enabled,
Expand All @@ -2502,13 +2502,36 @@ a `types.graphql` file will be written to your `assets/` directory on `flush`.
When `cache_types_in_filesystem` is enabled, it is recommended that you remove the extension that
provides the dynamic introspection endpoint.


```php
use SilverStripe\GraphQL\Controller;
use SilverStripe\GraphQL\Extensions\IntrospectionProvider;

Controller::remove_extension(IntrospectionProvider::class);
```

### Disabling Schema introspection

By default, schema introspection is only enabled in development environments. While schema intospection provides no
information that can be explicitly used for malicious purposes, it is best practice to disable it in production. This
is strongly encouraged by [OWASP guidelines](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html)
in the spirit of "security through obscurity."

In development environments, however, schema introspection is critical to a good developer experience, particularly in
the [graphql IDE](https://github.com/silverstripe/graphql-devtools).

If you would like to enable schema introspection outside of your dev environment, you can set the `allowIntrospection`
property on the `Manager` instance.

```html
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\Manager.default:
properties:
AllowIntrospection: true
madmatt marked this conversation as resolved.
Show resolved Hide resolved
```



## Setting up a new GraphQL schema

In addition to the default `/graphql` endpoint provided by this module by default,
Expand All @@ -2526,7 +2549,7 @@ SilverStripe\Core\Injector\Injector:
schemaKey: mySchema
```

The `schemaKey` setting is a bit of meta-configuration used to tell the Manager where to
The `schemaKey` setting is a bit of meta-configuration used to tell the Manager where to
look in the `SilverStripe\GraphQL\Manager.schemas` config for the schema information.

Now let's setup a new controller to handle the requests. It will use our custom Manager instance.
Expand Down
3 changes: 2 additions & 1 deletion src/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public function index(HTTPRequest $request)
Versioned::set_stage($stage);
}
}

// Check for a possible CORS preflight request and handle if necessary
// Refer issue 66: https://github.com/silverstripe/silverstripe-graphql/issues/66
if ($request->httpMethod() === 'OPTIONS') {
Expand Down Expand Up @@ -460,6 +460,7 @@ public function writeSchemaToFilesystem()
$request = new NullHTTPRequest();
}
$manager = $this->getManager($request);
$manager->setAllowIntrospection(true);
try {
$types = StaticSchema::inst()->introspectTypes($manager);
} catch (Exception $e) {
Expand Down
3 changes: 3 additions & 0 deletions src/Extensions/IntrospectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Extension;
use SilverStripe\GraphQL\Manager;
use SilverStripe\GraphQL\Scaffolding\StaticSchema;

/**
Expand All @@ -22,7 +23,9 @@ class IntrospectionProvider extends Extension
*/
public function types(HTTPRequest $request)
{
/* @var Manager $manager */
$manager = $this->owner->getManager();
$manager->setAllowIntrospection(true);
$fragments = StaticSchema::inst()->introspectTypes($manager);

return (new HTTPResponse(json_encode($fragments), 200))
Expand Down
32 changes: 32 additions & 0 deletions src/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace SilverStripe\GraphQL;

use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\DisableIntrospection;
use InvalidArgumentException;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Schema;
use GraphQL\GraphQL;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injector;
Expand Down Expand Up @@ -89,6 +92,11 @@ class Manager implements ConfigurationApplier
*/
protected $extraContext = [];

/**
* @var bool
*/
protected $allowIntrospection = false;

/**
* @return QueryMiddleware[]
*/
Expand Down Expand Up @@ -157,6 +165,7 @@ public function __construct($schemaKey = null)
if ($schemaKey) {
$this->setSchemaKey($schemaKey);
}
$this->setAllowIntrospection(!Director::isDev());
}

/**
Expand Down Expand Up @@ -335,6 +344,10 @@ public function schema()
]);
}

if (!$this->getAllowIntrospection()) {
DocumentValidator::addRule(new DisableIntrospection());
}

return new Schema($schema);
}

Expand Down Expand Up @@ -566,6 +579,25 @@ public function getQueryFromPersistedID($id)
return $provider->getByID($id);
}

/**
* @return bool
*/
public function getAllowIntrospection(): bool
{
return $this->allowIntrospection;
}

/**
* @param bool $allowIntrospection
* @return $this
*/
public function setAllowIntrospection(bool $allowIntrospection): self
{
$this->allowIntrospection = $allowIntrospection;

return $this;
}

/**
* Get global context to pass to $context for all queries
*
Expand Down