Strawberry GraphQL Cross-Site Request Forgery (CSRF) vulnerability
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.
| Package | Affected versions | Patched versions |
|---|---|---|
strawberry-graphqlPyPI | < 0.243.0 | 0.243.0 |
Affected products
1- Range: < 0.243.0
Patches
137265b230e51Disable multipart uploads by default (#3645)
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- github.com/advisories/GHSA-79gp-q4wv-33frghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-47082ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/strawberry-graphql/PYSEC-2024-171.yamlghsaWEB
- github.com/strawberry-graphql/strawberry/commit/37265b230e511480a9ceace492f9f6a484be1387ghsax_refsource_MISCWEB
- github.com/strawberry-graphql/strawberry/security/advisories/GHSA-79gp-q4wv-33frghsax_refsource_CONFIRMWEB
- strawberry.rocks/docs/breaking-changes/0.243.0ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.