Skip to content

Commit

Permalink
feat: disable graphql introspection queries
Browse files Browse the repository at this point in the history
GraphQL queries starting with `__` e.g. `__schema` will
be disabled by default.

Refs: HP-2350
  • Loading branch information
charn committed Apr 10, 2024
1 parent 04929c9 commit 4f65ed7
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Prerequisites:
* `CREATE_SUPERUSER`, creates a superuser with credentials `admin`:`admin` ([email protected])
* `APPLY_MIGRATIONS`, applies migrations on startup
* `ENABLE_GRAPHIQL`, enables GraphiQL interface for `/graphql/`
* `ENABLE_GRAPHQL_INTROSPECTION`, enables GraphQL introspection queries
* `SEED_DEVELOPMENT_DATA`, flush data and recreate the environment with
fake development data (requires `APPLY_MIGRATIONS`)
* `OIDC_CLIENT_ID`, Tunnistamo client id for enabling GDPR API authorization code flows
Expand Down
3 changes: 2 additions & 1 deletion docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Common environment variable that is required in either case:
== Feature flags

- `ENABLE_GRAPHIQL`: Enables GraphiQL testing user interface. If `DEBUG` is `True`, this setting has no effect and GraphiQL is always enabled. Default is `False`.
- `ENABLE_GRAPHQL_INTROSPECTION`: Enables GraphQL introspection queries. If `DEBUG` is `True`, this setting has no effect and introspection queries are always enabled. Default is `False`.
- `USE_X_FORWARDED_FOR`: Affects the way how a requester's IP address is figured out. If set to `True`, the `X-Forwarded-For` HTTP header is used as one option. Default is `False`.

== Sentry
Expand All @@ -111,7 +112,7 @@ It's possible to report errors to Sentry.

- `SENTRY_DSN`: Sets the https://docs.sentry.io/platforms/python/configuration/options/#dsn[Sentry DSN]. If this is not set, nothing is sent to Sentry.
- `SENTRY_ENVIRONMENT`: Sets the https://docs.sentry.io/platforms/python/configuration/options/#environment[Sentry environment]. Default is "development".
- `COMMIT_HASH`: Sets the https://docs.sentry.io/platforms/python/configuration/options/#release[Sentry release]. See `COMMIT_HASH` in <<Miscellaneous>>. If `COMMIT_HASH` is not set, set module version instead.
- `COMMIT_HASH`: Sets the https://docs.sentry.io/platforms/python/configuration/options/#release[Sentry release]. See `COMMIT_HASH` in <<Miscellaneous>>. If `COMMIT_HASH` is not set, set module version instead.

== Miscellaneous

Expand Down
5 changes: 4 additions & 1 deletion open_city_profile/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
AUDIT_LOG_TO_DB_ENABLED=(bool, False),
OPEN_CITY_PROFILE_LOG_LEVEL=(str, None),
ENABLE_GRAPHIQL=(bool, False),
ENABLE_GRAPHQL_INTROSPECTION=(bool, False),
FORCE_SCRIPT_NAME=(str, ""),
CSRF_COOKIE_NAME=(str, ""),
CSRF_COOKIE_PATH=(str, ""),
Expand Down Expand Up @@ -155,8 +156,10 @@

DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL")

# Set to True to enable GraphiQL interface, this will overriden to True if DEBUG=True
# Set to True to enable GraphiQL interface, enabled automatically if DEBUG=True
ENABLE_GRAPHIQL = env("ENABLE_GRAPHIQL")
# Enable GraphQL introspection queries, enabled automatically if DEBUG=True
ENABLE_GRAPHQL_INTROSPECTION = env("ENABLE_GRAPHQL_INTROSPECTION")

INSTALLED_APPS = [
"helusers.apps.HelusersConfig",
Expand Down
5 changes: 5 additions & 0 deletions open_city_profile/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ def email_setup(settings):
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"


@pytest.fixture(autouse=True)
def enable_instrospection_query(settings):
settings.ENABLE_GRAPHQL_INTROSPECTION = True


@pytest.fixture
def keycloak_setup(settings):
settings.KEYCLOAK_BASE_URL = "https://localhost/keycloak"
Expand Down
27 changes: 26 additions & 1 deletion open_city_profile/tests/test_graphql_api_schema.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
from graphql import print_schema
import pytest
import requests
from graphql import get_introspection_query, print_schema


def test_graphql_schema_matches_the_reference(gql_schema, snapshot):
actual_schema = print_schema(gql_schema)

snapshot.assert_match(actual_schema)


@pytest.mark.parametrize("enabled", [True, False])
def test_graphql_schema_introspection_can_be_disabled(live_server, settings, enabled):
settings.ENABLE_GRAPHQL_INTROSPECTION = enabled
url = live_server.url + "/graphql/"
payload = {
"query": get_introspection_query(),
}

response = requests.post(url, json=payload)

body = response.json()
if enabled:
assert response.status_code == 200
assert "errors" not in body
assert "__schema" in body["data"]
else:
assert response.status_code == 400
assert "data" not in body
error = body["errors"][0]["message"]
assert "__schema" in error
assert "introspection is disabled" in error
32 changes: 31 additions & 1 deletion open_city_profile/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import graphene_validator.errors
import sentry_sdk
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
from graphene.validation import DisableIntrospection
from graphene_django.views import GraphQLView as BaseGraphQLView
from graphql import ExecutionResult, parse, validate
from helusers.oidc import AuthenticationError

from open_city_profile.consts import (
Expand Down Expand Up @@ -90,9 +93,36 @@ def _get_error_code(exception):


class GraphQLView(BaseGraphQLView):

def _run_custom_validator(self, query):
result = None
if not settings.ENABLE_GRAPHQL_INTROSPECTION and not settings.DEBUG:
try:
document = parse(query)
except Exception:
# Execution will also fail in super().execute_graphql_request()
# when parsing the query so no need to do anything here.
pass
else:
validation_errors = validate(
schema=self.schema.graphql_schema,
document_ast=document,
rules=(DisableIntrospection,),
)
if validation_errors:
result = ExecutionResult(data=None, errors=validation_errors)

return result

def execute_graphql_request(self, request, data, query, *args, **kwargs):
"""Extract any exceptions and send some of them to Sentry"""
result = super().execute_graphql_request(request, data, query, *args, **kwargs)
result = self._run_custom_validator(query)

if not result:
result = super().execute_graphql_request(
request, data, query, *args, **kwargs
)

if result and result.errors:
errors = [
e
Expand Down

0 comments on commit 4f65ed7

Please sign in to comment.