VYPR
Moderate severityNVD Advisory· Published Sep 25, 2024· Updated Sep 25, 2024

Strawberry GraphQL Cross-Site Request Forgery (CSRF) vulnerability

CVE-2024-47082

Description

Strawberry GraphQL is a library for creating GraphQL APIs. Prior to version 0.243.0, multipart file upload support as defined in the GraphQL multipart request specification was enabled by default in all Strawberry HTTP view integrations. This made all Strawberry HTTP view integrations vulnerable to cross-site request forgery (CSRF) attacks if users did not explicitly enable CSRF preventing security mechanism for their servers. Additionally, the Django HTTP view integration, in particular, had an exemption for Django's built-in CSRF protection (i.e., the CsrfViewMiddleware middleware) by default. In affect, all Strawberry integrations were vulnerable to CSRF attacks by default. Version v0.243.0 is the first strawberry-graphql including a patch.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
strawberry-graphqlPyPI
< 0.243.00.243.0

Affected products

1

Patches

1
37265b230e51

Disable multipart uploads by default (#3645)

https://github.com/strawberry-graphql/strawberryJonathan EhwaldSep 25, 2024via ghsa
40 files changed · +207 44
  • docs/breaking-changes/0.243.0.md+53 0 added
    @@ -0,0 +1,53 @@
    +---
    +title: 0.243.0 Breaking Changes
    +slug: breaking-changes/0.243.0
    +---
    +
    +# v0.240.0 Breaking Changes
    +
    +Release v0.240.0 comes with two breaking changes regarding multipart file
    +uploads and Django CSRF protection.
    +
    +## Multipart uploads disabled by default
    +
    +Previously, support for uploads via the
    +[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec)
    +was enabled by default. This implicitly required Strawberry users to consider
    +the
    +[security implications outlined in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security).
    +Given that most Strawberry users were likely not aware of this, we're making
    +multipart file upload support stictly opt-in via a new
    +`multipart_uploads_enabled` view settings.
    +
    +To enable multipart upload support for your Strawberry view integration, please
    +follow the updated integration guides and enable appropriate security
    +measurements for your server.
    +
    +## Django CSRF protection enabled
    +
    +Previously, the Strawberry Django view integration was internally exempted from
    +Django's built-in CSRF protection (i.e, the `CsrfViewMiddleware` middleware).
    +While this is how many GraphQL APIs operate, implicitly addded exemptions can
    +lead to security vulnerabilities. Instead, we delegate the decision of adding an
    +CSRF exemption to users now.
    +
    +Note that having the CSRF protection enabled on your Strawberry Django view
    +potentially requires all your clients to send an CSRF token with every request.
    +You can learn more about this in the official Django
    +[Cross Site Request Forgery protection documentation](https://docs.djangoproject.com/en/dev/ref/csrf/).
    +
    +To restore the behaviour of the integration before this release, you can add the
    +`csrf_exempt` decorator provided by Django yourself:
    +
    +```python
    +from django.urls import path
    +from django.views.decorators.csrf import csrf_exempt
    +
    +from strawberry.django.views import GraphQLView
    +
    +from api.schema import schema
    +
    +urlpatterns = [
    +    path("graphql/", csrf_exempt(GraphQLView.as_view(schema=schema))),
    +]
    +```
    
  • docs/breaking-changes.md+1 0 modified
    @@ -4,6 +4,7 @@ title: List of breaking changes and deprecations
     
     # List of breaking changes and deprecations
     
    +- [Version 0.243.0 - 25 September 2024](./breaking-changes/0.243.0.md)
     - [Version 0.240.0 - 10 September 2024](./breaking-changes/0.240.0.md)
     - [Version 0.236.0 - 17 July 2024](./breaking-changes/0.236.0.md)
     - [Version 0.233.0 - 29 May 2024](./breaking-changes/0.233.0.md)
    
  • docs/integrations/aiohttp.md+5 1 modified
    @@ -29,14 +29,18 @@ app.router.add_route("*", "/graphql", GraphQLView(schema=schema))
     
     ## Options
     
    -The `GraphQLView` accepts two options at the moment:
    +The `GraphQLView` accepts the following options at the moment:
     
     - `schema`: mandatory, the schema created by `strawberry.Schema`.
     - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the
       GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or
       to disable it by passing `None`.
     - `allow_queries_via_get`: optional, defaults to `True`, whether to enable
       queries via `GET` requests
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## Extending the view
     
    
  • docs/integrations/asgi.md+5 1 modified
    @@ -29,14 +29,18 @@ app with `uvicorn server:app`
     
     ## Options
     
    -The `GraphQL` app accepts two options at the moment:
    +The `GraphQL` app accepts the following options at the moment:
     
     - `schema`: mandatory, the schema created by `strawberry.Schema`.
     - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the
       GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or
       to disable it by passing `None`.
     - `allow_queries_via_get`: optional, defaults to `True`, whether to enable
       queries via `GET` requests
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## Extending the view
     
    
  • docs/integrations/channels.md+4 0 modified
    @@ -524,6 +524,10 @@ GraphQLWebsocketCommunicator(
       queries via `GET` requests
     - `subscriptions_enabled`: optional boolean paramenter enabling subscriptions in
       the GraphiQL interface, defaults to `True`
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ### Extending the consumer
     
    
  • docs/integrations/django.md+6 1 modified
    @@ -10,13 +10,14 @@ It provides a view that you can use to serve your GraphQL schema:
     
     ```python
     from django.urls import path
    +from django.views.decorators.csrf import csrf_exempt
     
     from strawberry.django.views import GraphQLView
     
     from api.schema import schema
     
     urlpatterns = [
    -    path("graphql/", GraphQLView.as_view(schema=schema)),
    +    path("graphql/", csrf_exempt(GraphQLView.as_view(schema=schema))),
     ]
     ```
     
    @@ -40,6 +41,10 @@ The `GraphQLView` accepts the following arguments:
       queries via `GET` requests
     - `subscriptions_enabled`: optional boolean paramenter enabling subscriptions in
       the GraphiQL interface, defaults to `False`.
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## Deprecated options
     
    
  • docs/integrations/fastapi.md+4 0 modified
    @@ -54,6 +54,10 @@ The `GraphQLRouter` accepts the following options:
       value.
     - `root_value_getter`: optional FastAPI dependency for providing custom root
       value.
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## context_getter
     
    
  • docs/integrations/flask.md+5 1 modified
    @@ -34,13 +34,17 @@ from strawberry.flask.views import AsyncGraphQLView
     
     ## Options
     
    -The `GraphQLView` accepts two options at the moment:
    +The `GraphQLView` accepts the following options at the moment:
     
     - `schema`: mandatory, the schema created by `strawberry.Schema`.
     - `graphiql:` optional, defaults to `True`, whether to enable the GraphiQL
       interface.
     - `allow_queries_via_get`: optional, defaults to `True`, whether to enable
       queries via `GET` requests
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## Extending the view
     
    
  • docs/integrations/litestar.md+4 0 modified
    @@ -61,6 +61,10 @@ The `make_graphql_controller` function accepts the following options:
       the maximum time to wait for the connection initialization message when using
       `graphql-transport-ws`
       [protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#connectioninit)
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## context_getter
     
    
  • docs/integrations/quart.md+5 1 modified
    @@ -26,13 +26,17 @@ if __name__ == "__main__":
     
     ## Options
     
    -The `GraphQLView` accepts two options at the moment:
    +The `GraphQLView` accepts the following options at the moment:
     
     - `schema`: mandatory, the schema created by `strawberry.Schema`.
     - `graphiql:` optional, defaults to `True`, whether to enable the GraphiQL
       interface.
     - `allow_queries_via_get`: optional, defaults to `True`, whether to enable
       queries via `GET` requests
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## Extending the view
     
    
  • docs/integrations/sanic.md+5 2 modified
    @@ -22,15 +22,18 @@ app.add_route(
     
     ## Options
     
    -The `GraphQLView` accepts two options at the moment:
    +The `GraphQLView` accepts the following options at the moment:
     
     - `schema`: mandatory, the schema created by `strawberry.Schema`.
     - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the
       GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or
       to disable it by passing `None`.
     - `allow_queries_via_get`: optional, defaults to `True`, whether to enable
       queries via `GET` requests
    -- `def encode_json(self, data: GraphQLHTTPResponse) -> str`
    +- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
    +  to enable multipart uploads. Please make sure to consider the
    +  [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
    +  when enabling this feature.
     
     ## Extending the view
     
    
  • .github/workflows/test.yml+0 14 modified
    @@ -59,20 +59,6 @@ jobs:
                 3.12
                 3.13-dev
     
    -      - name: Pip and nox cache
    -        id: cache
    -        uses: actions/cache@v4
    -        with:
    -          path: |
    -            ~/.cache
    -            ~/.nox
    -            .nox
    -          key:
    -            ${{ runner.os }}-nox-${{ matrix.session.session }}-${{ env.pythonLocation }}-${{
    -            hashFiles('**/poetry.lock') }}-${{ hashFiles('**/noxfile.py') }}
    -          restore-keys: |
    -            ${{ runner.os }}-nox-${{ matrix.session.session }}-${{ env.pythonLocation }}
    -
           - run: pip install poetry nox nox-poetry uv
           - run: nox -r -t tests -s "${{ matrix.session.session }}"
           - uses: actions/upload-artifact@v4
    
  • RELEASE.md+7 0 added
    @@ -0,0 +1,7 @@
    +Release type: minor
    +
    +Starting with this release, multipart uploads are disabled by default and Strawberry Django view is no longer implicitly exempted from Django's CSRF protection.
    +Both changes relieve users from implicit security implications inherited from the GraphQL multipart request specification which was enabled in Strawberry by default.
    +
    +These are breaking changes if you are using multipart uploads OR the Strawberry Django view.
    +Migrations guides including further information are available on the Strawberry website.
    
  • strawberry/aiohttp/views.py+2 0 modified
    @@ -111,6 +111,7 @@ def __init__(
                 GRAPHQL_WS_PROTOCOL,
             ),
             connection_init_wait_timeout: timedelta = timedelta(minutes=1),
    +        multipart_uploads_enabled: bool = False,
         ) -> None:
             self.schema = schema
             self.allow_queries_via_get = allow_queries_via_get
    @@ -119,6 +120,7 @@ def __init__(
             self.debug = debug
             self.subscription_protocols = subscription_protocols
             self.connection_init_wait_timeout = connection_init_wait_timeout
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if graphiql is not None:
                 warnings.warn(
    
  • strawberry/asgi/__init__.py+2 0 modified
    @@ -106,6 +106,7 @@ def __init__(
                 GRAPHQL_WS_PROTOCOL,
             ),
             connection_init_wait_timeout: timedelta = timedelta(minutes=1),
    +        multipart_uploads_enabled: bool = False,
         ) -> None:
             self.schema = schema
             self.allow_queries_via_get = allow_queries_via_get
    @@ -114,6 +115,7 @@ def __init__(
             self.debug = debug
             self.protocols = subscription_protocols
             self.connection_init_wait_timeout = connection_init_wait_timeout
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if graphiql is not None:
                 warnings.warn(
    
  • strawberry/channels/handlers/http_handler.py+2 0 modified
    @@ -168,12 +168,14 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             subscriptions_enabled: bool = True,
    +        multipart_uploads_enabled: bool = False,
             **kwargs: Any,
         ) -> None:
             self.schema = schema
             self.allow_queries_via_get = allow_queries_via_get
             self.subscriptions_enabled = subscriptions_enabled
             self._ide_subscriptions_enabled = subscriptions_enabled
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if graphiql is not None:
                 warnings.warn(
    
  • strawberry/django/views.py+3 4 modified
    @@ -28,8 +28,7 @@
     from django.template.exceptions import TemplateDoesNotExist
     from django.template.loader import render_to_string
     from django.template.response import TemplateResponse
    -from django.utils.decorators import classonlymethod, method_decorator
    -from django.views.decorators.csrf import csrf_exempt
    +from django.utils.decorators import classonlymethod
     from django.views.generic import View
     
     from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
    @@ -147,11 +146,13 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             subscriptions_enabled: bool = False,
    +        multipart_uploads_enabled: bool = False,
             **kwargs: Any,
         ) -> None:
             self.schema = schema
             self.allow_queries_via_get = allow_queries_via_get
             self.subscriptions_enabled = subscriptions_enabled
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if graphiql is not None:
                 warnings.warn(
    @@ -229,7 +230,6 @@ def get_context(self, request: HttpRequest, response: HttpResponse) -> Any:
         def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse:
             return TemporalHttpResponse()
     
    -    @method_decorator(csrf_exempt)
         def dispatch(
             self, request: HttpRequest, *args: Any, **kwargs: Any
         ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponseBase]:
    @@ -288,7 +288,6 @@ async def get_context(self, request: HttpRequest, response: HttpResponse) -> Any
         async def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse:
             return TemporalHttpResponse()
     
    -    @method_decorator(csrf_exempt)
         async def dispatch(  # pyright: ignore
             self, request: HttpRequest, *args: Any, **kwargs: Any
         ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponseBase]:
    
  • strawberry/fastapi/router.py+2 0 modified
    @@ -156,6 +156,7 @@ def __init__(
             generate_unique_id_function: Callable[[APIRoute], str] = Default(
                 generate_unique_id
             ),
    +        multipart_uploads_enabled: bool = False,
             **kwargs: Any,
         ) -> None:
             super().__init__(
    @@ -190,6 +191,7 @@ def __init__(
             )
             self.protocols = subscription_protocols
             self.connection_init_wait_timeout = connection_init_wait_timeout
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if graphiql is not None:
                 warnings.warn(
    
  • strawberry/flask/views.py+2 0 modified
    @@ -71,10 +71,12 @@ def __init__(
             graphiql: Optional[bool] = None,
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
    +        multipart_uploads_enabled: bool = False,
         ) -> None:
             self.schema = schema
             self.graphiql = graphiql
             self.allow_queries_via_get = allow_queries_via_get
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if graphiql is not None:
                 warnings.warn(
    
  • strawberry/http/async_base_view.py+1 1 modified
    @@ -333,7 +333,7 @@ async def parse_http_body(
                 data = self.parse_query_params(request.query_params)
             elif "application/json" in content_type:
                 data = self.parse_json(await request.get_body())
    -        elif content_type == "multipart/form-data":
    +        elif self.multipart_uploads_enabled and content_type == "multipart/form-data":
                 data = await self.parse_multipart(request)
             else:
                 raise HTTPException(400, "Unsupported content type")
    
  • strawberry/http/base.py+1 0 modified
    @@ -23,6 +23,7 @@ def headers(self) -> Mapping[str, str]: ...
     
     class BaseView(Generic[Request]):
         graphql_ide: Optional[GraphQL_IDE]
    +    multipart_uploads_enabled: bool = False
     
         # TODO: we might remove this in future :)
         _ide_replace_variables: bool = True
    
  • strawberry/http/sync_base_view.py+1 1 modified
    @@ -143,7 +143,7 @@ def parse_http_body(self, request: SyncHTTPRequestAdapter) -> GraphQLRequestData
             elif "application/json" in content_type:
                 data = self.parse_json(request.body)
             # TODO: multipart via get?
    -        elif content_type == "multipart/form-data":
    +        elif self.multipart_uploads_enabled and content_type == "multipart/form-data":
                 data = self.parse_multipart(request)
             elif self._is_multipart_subscriptions(content_type, params):
                 raise HTTPException(
    
  • strawberry/litestar/controller.py+2 0 modified
    @@ -410,6 +410,7 @@ def make_graphql_controller(
             GRAPHQL_WS_PROTOCOL,
         ),
         connection_init_wait_timeout: timedelta = timedelta(minutes=1),
    +    multipart_uploads_enabled: bool = False,
     ) -> Type[GraphQLController]:  # sourcery skip: move-assign
         if context_getter is None:
             custom_context_getter_ = _none_custom_context_getter
    @@ -456,6 +457,7 @@ class _GraphQLController(GraphQLController):
         _GraphQLController.schema = schema_
         _GraphQLController.allow_queries_via_get = allow_queries_via_get_
         _GraphQLController.graphql_ide = graphql_ide_
    +    _GraphQLController.multipart_uploads_enabled = multipart_uploads_enabled
     
         return _GraphQLController
     
    
  • strawberry/quart/views.py+2 0 modified
    @@ -61,9 +61,11 @@ def __init__(
             graphiql: Optional[bool] = None,
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
    +        multipart_uploads_enabled: bool = False,
         ) -> None:
             self.schema = schema
             self.allow_queries_via_get = allow_queries_via_get
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if graphiql is not None:
                 warnings.warn(
    
  • strawberry/sanic/views.py+2 0 modified
    @@ -102,11 +102,13 @@ def __init__(
             allow_queries_via_get: bool = True,
             json_encoder: Optional[Type[json.JSONEncoder]] = None,
             json_dumps_params: Optional[Dict[str, Any]] = None,
    +        multipart_uploads_enabled: bool = False,
         ) -> None:
             self.schema = schema
             self.allow_queries_via_get = allow_queries_via_get
             self.json_encoder = json_encoder
             self.json_dumps_params = json_dumps_params
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
             if self.json_encoder is not None:  # pragma: no cover
                 warnings.warn(
    
  • tests/http/clients/aiohttp.py+2 0 modified
    @@ -72,13 +72,15 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             view = GraphQLView(
                 schema=schema,
                 graphiql=graphiql,
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 keep_alive=False,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
             view.result_override = result_override
     
    
  • tests/http/clients/asgi.py+2 0 modified
    @@ -74,13 +74,15 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             view = GraphQLView(
                 schema,
                 graphiql=graphiql,
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 keep_alive=False,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
             view.result_override = result_override
     
    
  • tests/http/clients/async_django.py+1 0 modified
    @@ -43,6 +43,7 @@ async def _do_request(self, request: RequestFactory) -> Response:
                 graphql_ide=self.graphql_ide,
                 allow_queries_via_get=self.allow_queries_via_get,
                 result_override=self.result_override,
    +            multipart_uploads_enabled=self.multipart_uploads_enabled,
             )
     
             try:
    
  • tests/http/clients/async_flask.py+2 0 modified
    @@ -52,6 +52,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.app = Flask(__name__)
             self.app.debug = True
    @@ -63,6 +64,7 @@ def __init__(
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 result_override=result_override,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
     
             self.app.add_url_rule(
    
  • tests/http/clients/base.py+1 0 modified
    @@ -103,6 +103,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ): ...
     
         @abc.abstractmethod
    
  • tests/http/clients/chalice.py+1 0 modified
    @@ -50,6 +50,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.app = Chalice(app_name="TheStackBadger")
     
    
  • tests/http/clients/channels.py+4 0 modified
    @@ -139,6 +139,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.ws_app = DebuggableGraphQLTransportWSConsumer.as_asgi(
                 schema=schema,
    @@ -151,6 +152,7 @@ def __init__(
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 result_override=result_override,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
     
         def create_app(self, **kwargs: Any) -> None:
    @@ -260,13 +262,15 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.http_app = DebuggableSyncGraphQLHTTPConsumer.as_asgi(
                 schema=schema,
                 graphiql=graphiql,
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 result_override=result_override,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
     
     
    
  • tests/http/clients/django.py+3 0 modified
    @@ -48,11 +48,13 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.graphiql = graphiql
             self.graphql_ide = graphql_ide
             self.allow_queries_via_get = allow_queries_via_get
             self.result_override = result_override
    +        self.multipart_uploads_enabled = multipart_uploads_enabled
     
         def _get_header_name(self, key: str) -> str:
             return f"HTTP_{key.upper().replace('-', '_')}"
    @@ -75,6 +77,7 @@ async def _do_request(self, request: RequestFactory) -> Response:
                 graphql_ide=self.graphql_ide,
                 allow_queries_via_get=self.allow_queries_via_get,
                 result_override=self.result_override,
    +            multipart_uploads_enabled=self.multipart_uploads_enabled,
             )
     
             try:
    
  • tests/http/clients/fastapi.py+2 0 modified
    @@ -86,6 +86,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.app = FastAPI()
     
    @@ -97,6 +98,7 @@ def __init__(
                 root_value_getter=get_root_value,
                 allow_queries_via_get=allow_queries_via_get,
                 keep_alive=False,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
             graphql_app.result_override = result_override
             self.app.include_router(graphql_app, prefix="/graphql")
    
  • tests/http/clients/flask.py+2 0 modified
    @@ -61,6 +61,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.app = Flask(__name__)
             self.app.debug = True
    @@ -72,6 +73,7 @@ def __init__(
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 result_override=result_override,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
     
             self.app.add_url_rule(
    
  • tests/http/clients/litestar.py+2 0 modified
    @@ -59,12 +59,14 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.create_app(
                 graphiql=graphiql,
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 result_override=result_override,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
     
         def create_app(self, result_override: ResultOverrideFunction = None, **kwargs: Any):
    
  • tests/http/clients/quart.py+2 0 modified
    @@ -54,6 +54,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.app = Quart(__name__)
             self.app.debug = True
    @@ -65,6 +66,7 @@ def __init__(
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 result_override=result_override,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
     
             self.app.add_url_rule(
    
  • tests/http/clients/sanic.py+2 0 modified
    @@ -53,6 +53,7 @@ def __init__(
             graphql_ide: Optional[GraphQL_IDE] = "graphiql",
             allow_queries_via_get: bool = True,
             result_override: ResultOverrideFunction = None,
    +        multipart_uploads_enabled: bool = False,
         ):
             self.app = Sanic(
                 f"test_{int(randint(0, 1000))}",  # noqa: S311
    @@ -63,6 +64,7 @@ def __init__(
                 graphql_ide=graphql_ide,
                 allow_queries_via_get=allow_queries_via_get,
                 result_override=result_override,
    +            multipart_uploads_enabled=multipart_uploads_enabled,
             )
             self.app.add_route(
                 view,
    
  • tests/http/test_upload.py+47 17 modified
    @@ -20,7 +20,18 @@ def http_client(http_client_class: Type[HttpClient]) -> HttpClient:
         return http_client_class()
     
     
    -async def test_upload(http_client: HttpClient):
    +@pytest.fixture()
    +def enabled_http_client(http_client_class: Type[HttpClient]) -> HttpClient:
    +    with contextlib.suppress(ImportError):
    +        from .clients.chalice import ChaliceHttpClient
    +
    +        if http_client_class is ChaliceHttpClient:
    +            pytest.xfail(reason="Chalice does not support uploads")
    +
    +    return http_client_class(multipart_uploads_enabled=True)
    +
    +
    +async def test_multipart_uploads_are_disabled_by_default(http_client: HttpClient):
         f = BytesIO(b"strawberry")
     
         query = """
    @@ -35,16 +46,35 @@ async def test_upload(http_client: HttpClient):
             files={"textFile": f},
         )
     
    +    assert response.status_code == 400
    +    assert response.data == b"Unsupported content type"
    +
    +
    +async def test_upload(enabled_http_client: HttpClient):
    +    f = BytesIO(b"strawberry")
    +
    +    query = """
    +    mutation($textFile: Upload!) {
    +        readText(textFile: $textFile)
    +    }
    +    """
    +
    +    response = await enabled_http_client.query(
    +        query,
    +        variables={"textFile": None},
    +        files={"textFile": f},
    +    )
    +
         assert response.json.get("errors") is None
         assert response.json["data"] == {"readText": "strawberry"}
     
     
    -async def test_file_list_upload(http_client: HttpClient):
    +async def test_file_list_upload(enabled_http_client: HttpClient):
         query = "mutation($files: [Upload!]!) { readFiles(files: $files) }"
         file1 = BytesIO(b"strawberry1")
         file2 = BytesIO(b"strawberry2")
     
    -    response = await http_client.query(
    +    response = await enabled_http_client.query(
             query=query,
             variables={"files": [None, None]},
             files={"file1": file1, "file2": file2},
    @@ -57,12 +87,12 @@ async def test_file_list_upload(http_client: HttpClient):
         assert data["readFiles"][1] == "strawberry2"
     
     
    -async def test_nested_file_list(http_client: HttpClient):
    +async def test_nested_file_list(enabled_http_client: HttpClient):
         query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }"
         file1 = BytesIO(b"strawberry1")
         file2 = BytesIO(b"strawberry2")
     
    -    response = await http_client.query(
    +    response = await enabled_http_client.query(
             query=query,
             variables={"folder": {"files": [None, None]}},
             files={"file1": file1, "file2": file2},
    @@ -74,7 +104,7 @@ async def test_nested_file_list(http_client: HttpClient):
         assert data["readFolder"][1] == "strawberry2"
     
     
    -async def test_upload_single_and_list_file_together(http_client: HttpClient):
    +async def test_upload_single_and_list_file_together(enabled_http_client: HttpClient):
         query = """
             mutation($files: [Upload!]!, $textFile: Upload!) {
                 readFiles(files: $files)
    @@ -85,7 +115,7 @@ async def test_upload_single_and_list_file_together(http_client: HttpClient):
         file2 = BytesIO(b"strawberry2")
         file3 = BytesIO(b"strawberry3")
     
    -    response = await http_client.query(
    +    response = await enabled_http_client.query(
             query=query,
             variables={"files": [None, None], "textFile": None},
             files={"file1": file1, "file2": file2, "textFile": file3},
    @@ -98,15 +128,15 @@ async def test_upload_single_and_list_file_together(http_client: HttpClient):
         assert data["readText"] == "strawberry3"
     
     
    -async def test_upload_invalid_query(http_client: HttpClient):
    +async def test_upload_invalid_query(enabled_http_client: HttpClient):
         f = BytesIO(b"strawberry")
     
         query = """
         mutation($textFile: Upload!) {
             readT
         """
     
    -    response = await http_client.query(
    +    response = await enabled_http_client.query(
             query,
             variables={"textFile": None},
             files={"textFile": f},
    @@ -122,7 +152,7 @@ async def test_upload_invalid_query(http_client: HttpClient):
         ]
     
     
    -async def test_upload_missing_file(http_client: HttpClient):
    +async def test_upload_missing_file(enabled_http_client: HttpClient):
         f = BytesIO(b"strawberry")
     
         query = """
    @@ -131,7 +161,7 @@ async def test_upload_missing_file(http_client: HttpClient):
         }
         """
     
    -    response = await http_client.query(
    +    response = await enabled_http_client.query(
             query,
             variables={"textFile": None},
             # using the wrong name to simulate a missing file
    @@ -155,7 +185,7 @@ def value(self) -> bytes:
             return self.buffer.getvalue()
     
     
    -async def test_extra_form_data_fields_are_ignored(http_client: HttpClient):
    +async def test_extra_form_data_fields_are_ignored(enabled_http_client: HttpClient):
         query = """mutation($textFile: Upload!) {
             readText(textFile: $textFile)
         }"""
    @@ -175,7 +205,7 @@ async def test_extra_form_data_fields_are_ignored(http_client: HttpClient):
     
         data, header = encode_multipart_formdata(fields)
     
    -    response = await http_client.post(
    +    response = await enabled_http_client.post(
             url="/graphql",
             data=data,
             headers={
    @@ -188,9 +218,9 @@ async def test_extra_form_data_fields_are_ignored(http_client: HttpClient):
         assert response.json["data"] == {"readText": "strawberry"}
     
     
    -async def test_sending_invalid_form_data(http_client: HttpClient):
    +async def test_sending_invalid_form_data(enabled_http_client: HttpClient):
         headers = {"content-type": "multipart/form-data; boundary=----fake"}
    -    response = await http_client.post("/graphql", headers=headers)
    +    response = await enabled_http_client.post("/graphql", headers=headers)
     
         assert response.status_code == 400
         # TODO: consolidate this, it seems only AIOHTTP returns the second error
    @@ -202,7 +232,7 @@ async def test_sending_invalid_form_data(http_client: HttpClient):
     
     
     @pytest.mark.aiohttp
    -async def test_sending_invalid_json_body(http_client: HttpClient):
    +async def test_sending_invalid_json_body(enabled_http_client: HttpClient):
         f = BytesIO(b"strawberry")
         operations = "}"
         file_map = json.dumps({"textFile": ["variables.textFile"]})
    @@ -215,7 +245,7 @@ async def test_sending_invalid_json_body(http_client: HttpClient):
     
         data, header = encode_multipart_formdata(fields)
     
    -    response = await http_client.post(
    +    response = await enabled_http_client.post(
             "/graphql",
             data=data,
             headers={
    
  • TWEET.md+8 0 added
    @@ -0,0 +1,8 @@
    +🆕 Release $version is out! Thanks to $contributor 👏
    +
    +We've made some important security changes regarding file uploads and CSRF in
    +Django.
    +
    +Check out our migration guides if you're using multipart or Django view.
    +
    +👇 $release_url
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.