Skip to content

Commit 37265b2

Browse files
DoctorJohnpre-commit-ci[bot]patrick91
authored
Disable multipart uploads by default (#3645)
* Disable multipart uploads by default * Document the new option * Stop disabling Django's CSRF protection by default * Document breaking changes * Add release file * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bump date * Test * Add tweet file * Shorter tweet --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Patrick Arminio <[email protected]>
1 parent 18f0f5d commit 37265b2

40 files changed

+207
-44
lines changed

.github/workflows/test.yml

-14
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,6 @@ jobs:
5959
3.12
6060
3.13-dev
6161
62-
- name: Pip and nox cache
63-
id: cache
64-
uses: actions/cache@v4
65-
with:
66-
path: |
67-
~/.cache
68-
~/.nox
69-
.nox
70-
key:
71-
${{ runner.os }}-nox-${{ matrix.session.session }}-${{ env.pythonLocation }}-${{
72-
hashFiles('**/poetry.lock') }}-${{ hashFiles('**/noxfile.py') }}
73-
restore-keys: |
74-
${{ runner.os }}-nox-${{ matrix.session.session }}-${{ env.pythonLocation }}
75-
7662
- run: pip install poetry nox nox-poetry uv
7763
- run: nox -r -t tests -s "${{ matrix.session.session }}"
7864
- uses: actions/upload-artifact@v4

RELEASE.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Release type: minor
2+
3+
Starting with this release, multipart uploads are disabled by default and Strawberry Django view is no longer implicitly exempted from Django's CSRF protection.
4+
Both changes relieve users from implicit security implications inherited from the GraphQL multipart request specification which was enabled in Strawberry by default.
5+
6+
These are breaking changes if you are using multipart uploads OR the Strawberry Django view.
7+
Migrations guides including further information are available on the Strawberry website.

TWEET.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
🆕 Release $version is out! Thanks to $contributor 👏
2+
3+
We've made some important security changes regarding file uploads and CSRF in
4+
Django.
5+
6+
Check out our migration guides if you're using multipart or Django view.
7+
8+
👇 $release_url

docs/breaking-changes.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ title: List of breaking changes and deprecations
44

55
# List of breaking changes and deprecations
66

7+
- [Version 0.243.0 - 25 September 2024](./breaking-changes/0.243.0.md)
78
- [Version 0.240.0 - 10 September 2024](./breaking-changes/0.240.0.md)
89
- [Version 0.236.0 - 17 July 2024](./breaking-changes/0.236.0.md)
910
- [Version 0.233.0 - 29 May 2024](./breaking-changes/0.233.0.md)

docs/breaking-changes/0.243.0.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
title: 0.243.0 Breaking Changes
3+
slug: breaking-changes/0.243.0
4+
---
5+
6+
# v0.240.0 Breaking Changes
7+
8+
Release v0.240.0 comes with two breaking changes regarding multipart file
9+
uploads and Django CSRF protection.
10+
11+
## Multipart uploads disabled by default
12+
13+
Previously, support for uploads via the
14+
[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec)
15+
was enabled by default. This implicitly required Strawberry users to consider
16+
the
17+
[security implications outlined in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security).
18+
Given that most Strawberry users were likely not aware of this, we're making
19+
multipart file upload support stictly opt-in via a new
20+
`multipart_uploads_enabled` view settings.
21+
22+
To enable multipart upload support for your Strawberry view integration, please
23+
follow the updated integration guides and enable appropriate security
24+
measurements for your server.
25+
26+
## Django CSRF protection enabled
27+
28+
Previously, the Strawberry Django view integration was internally exempted from
29+
Django's built-in CSRF protection (i.e, the `CsrfViewMiddleware` middleware).
30+
While this is how many GraphQL APIs operate, implicitly addded exemptions can
31+
lead to security vulnerabilities. Instead, we delegate the decision of adding an
32+
CSRF exemption to users now.
33+
34+
Note that having the CSRF protection enabled on your Strawberry Django view
35+
potentially requires all your clients to send an CSRF token with every request.
36+
You can learn more about this in the official Django
37+
[Cross Site Request Forgery protection documentation](https://docs.djangoproject.com/en/dev/ref/csrf/).
38+
39+
To restore the behaviour of the integration before this release, you can add the
40+
`csrf_exempt` decorator provided by Django yourself:
41+
42+
```python
43+
from django.urls import path
44+
from django.views.decorators.csrf import csrf_exempt
45+
46+
from strawberry.django.views import GraphQLView
47+
48+
from api.schema import schema
49+
50+
urlpatterns = [
51+
path("graphql/", csrf_exempt(GraphQLView.as_view(schema=schema))),
52+
]
53+
```

docs/integrations/aiohttp.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ app.router.add_route("*", "/graphql", GraphQLView(schema=schema))
2929

3030
## Options
3131

32-
The `GraphQLView` accepts two options at the moment:
32+
The `GraphQLView` accepts the following options at the moment:
3333

3434
- `schema`: mandatory, the schema created by `strawberry.Schema`.
3535
- `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the
3636
GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or
3737
to disable it by passing `None`.
3838
- `allow_queries_via_get`: optional, defaults to `True`, whether to enable
3939
queries via `GET` requests
40+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
41+
to enable multipart uploads. Please make sure to consider the
42+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
43+
when enabling this feature.
4044

4145
## Extending the view
4246

docs/integrations/asgi.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ app with `uvicorn server:app`
2929

3030
## Options
3131

32-
The `GraphQL` app accepts two options at the moment:
32+
The `GraphQL` app accepts the following options at the moment:
3333

3434
- `schema`: mandatory, the schema created by `strawberry.Schema`.
3535
- `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the
3636
GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or
3737
to disable it by passing `None`.
3838
- `allow_queries_via_get`: optional, defaults to `True`, whether to enable
3939
queries via `GET` requests
40+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
41+
to enable multipart uploads. Please make sure to consider the
42+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
43+
when enabling this feature.
4044

4145
## Extending the view
4246

docs/integrations/channels.md

+4
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,10 @@ GraphQLWebsocketCommunicator(
524524
queries via `GET` requests
525525
- `subscriptions_enabled`: optional boolean paramenter enabling subscriptions in
526526
the GraphiQL interface, defaults to `True`
527+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
528+
to enable multipart uploads. Please make sure to consider the
529+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
530+
when enabling this feature.
527531

528532
### Extending the consumer
529533

docs/integrations/django.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ It provides a view that you can use to serve your GraphQL schema:
1010

1111
```python
1212
from django.urls import path
13+
from django.views.decorators.csrf import csrf_exempt
1314

1415
from strawberry.django.views import GraphQLView
1516

1617
from api.schema import schema
1718

1819
urlpatterns = [
19-
path("graphql/", GraphQLView.as_view(schema=schema)),
20+
path("graphql/", csrf_exempt(GraphQLView.as_view(schema=schema))),
2021
]
2122
```
2223

@@ -40,6 +41,10 @@ The `GraphQLView` accepts the following arguments:
4041
queries via `GET` requests
4142
- `subscriptions_enabled`: optional boolean paramenter enabling subscriptions in
4243
the GraphiQL interface, defaults to `False`.
44+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
45+
to enable multipart uploads. Please make sure to consider the
46+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
47+
when enabling this feature.
4348

4449
## Deprecated options
4550

docs/integrations/fastapi.md

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ The `GraphQLRouter` accepts the following options:
5454
value.
5555
- `root_value_getter`: optional FastAPI dependency for providing custom root
5656
value.
57+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
58+
to enable multipart uploads. Please make sure to consider the
59+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
60+
when enabling this feature.
5761

5862
## context_getter
5963

docs/integrations/flask.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,17 @@ from strawberry.flask.views import AsyncGraphQLView
3434

3535
## Options
3636

37-
The `GraphQLView` accepts two options at the moment:
37+
The `GraphQLView` accepts the following options at the moment:
3838

3939
- `schema`: mandatory, the schema created by `strawberry.Schema`.
4040
- `graphiql:` optional, defaults to `True`, whether to enable the GraphiQL
4141
interface.
4242
- `allow_queries_via_get`: optional, defaults to `True`, whether to enable
4343
queries via `GET` requests
44+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
45+
to enable multipart uploads. Please make sure to consider the
46+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
47+
when enabling this feature.
4448

4549
## Extending the view
4650

docs/integrations/litestar.md

+4
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ The `make_graphql_controller` function accepts the following options:
6161
the maximum time to wait for the connection initialization message when using
6262
`graphql-transport-ws`
6363
[protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#connectioninit)
64+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
65+
to enable multipart uploads. Please make sure to consider the
66+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
67+
when enabling this feature.
6468

6569
## context_getter
6670

docs/integrations/quart.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ if __name__ == "__main__":
2626

2727
## Options
2828

29-
The `GraphQLView` accepts two options at the moment:
29+
The `GraphQLView` accepts the following options at the moment:
3030

3131
- `schema`: mandatory, the schema created by `strawberry.Schema`.
3232
- `graphiql:` optional, defaults to `True`, whether to enable the GraphiQL
3333
interface.
3434
- `allow_queries_via_get`: optional, defaults to `True`, whether to enable
3535
queries via `GET` requests
36+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
37+
to enable multipart uploads. Please make sure to consider the
38+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
39+
when enabling this feature.
3640

3741
## Extending the view
3842

docs/integrations/sanic.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@ app.add_route(
2222

2323
## Options
2424

25-
The `GraphQLView` accepts two options at the moment:
25+
The `GraphQLView` accepts the following options at the moment:
2626

2727
- `schema`: mandatory, the schema created by `strawberry.Schema`.
2828
- `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the
2929
GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or
3030
to disable it by passing `None`.
3131
- `allow_queries_via_get`: optional, defaults to `True`, whether to enable
3232
queries via `GET` requests
33-
- `def encode_json(self, data: GraphQLHTTPResponse) -> str`
33+
- `multipart_uploads_enabled`: optional, defaults to `False`, controls whether
34+
to enable multipart uploads. Please make sure to consider the
35+
[security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security)
36+
when enabling this feature.
3437

3538
## Extending the view
3639

strawberry/aiohttp/views.py

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def __init__(
111111
GRAPHQL_WS_PROTOCOL,
112112
),
113113
connection_init_wait_timeout: timedelta = timedelta(minutes=1),
114+
multipart_uploads_enabled: bool = False,
114115
) -> None:
115116
self.schema = schema
116117
self.allow_queries_via_get = allow_queries_via_get
@@ -119,6 +120,7 @@ def __init__(
119120
self.debug = debug
120121
self.subscription_protocols = subscription_protocols
121122
self.connection_init_wait_timeout = connection_init_wait_timeout
123+
self.multipart_uploads_enabled = multipart_uploads_enabled
122124

123125
if graphiql is not None:
124126
warnings.warn(

strawberry/asgi/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def __init__(
106106
GRAPHQL_WS_PROTOCOL,
107107
),
108108
connection_init_wait_timeout: timedelta = timedelta(minutes=1),
109+
multipart_uploads_enabled: bool = False,
109110
) -> None:
110111
self.schema = schema
111112
self.allow_queries_via_get = allow_queries_via_get
@@ -114,6 +115,7 @@ def __init__(
114115
self.debug = debug
115116
self.protocols = subscription_protocols
116117
self.connection_init_wait_timeout = connection_init_wait_timeout
118+
self.multipart_uploads_enabled = multipart_uploads_enabled
117119

118120
if graphiql is not None:
119121
warnings.warn(

strawberry/channels/handlers/http_handler.py

+2
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,14 @@ def __init__(
168168
graphql_ide: Optional[GraphQL_IDE] = "graphiql",
169169
allow_queries_via_get: bool = True,
170170
subscriptions_enabled: bool = True,
171+
multipart_uploads_enabled: bool = False,
171172
**kwargs: Any,
172173
) -> None:
173174
self.schema = schema
174175
self.allow_queries_via_get = allow_queries_via_get
175176
self.subscriptions_enabled = subscriptions_enabled
176177
self._ide_subscriptions_enabled = subscriptions_enabled
178+
self.multipart_uploads_enabled = multipart_uploads_enabled
177179

178180
if graphiql is not None:
179181
warnings.warn(

strawberry/django/views.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@
2828
from django.template.exceptions import TemplateDoesNotExist
2929
from django.template.loader import render_to_string
3030
from django.template.response import TemplateResponse
31-
from django.utils.decorators import classonlymethod, method_decorator
32-
from django.views.decorators.csrf import csrf_exempt
31+
from django.utils.decorators import classonlymethod
3332
from django.views.generic import View
3433

3534
from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
@@ -147,11 +146,13 @@ def __init__(
147146
graphql_ide: Optional[GraphQL_IDE] = "graphiql",
148147
allow_queries_via_get: bool = True,
149148
subscriptions_enabled: bool = False,
149+
multipart_uploads_enabled: bool = False,
150150
**kwargs: Any,
151151
) -> None:
152152
self.schema = schema
153153
self.allow_queries_via_get = allow_queries_via_get
154154
self.subscriptions_enabled = subscriptions_enabled
155+
self.multipart_uploads_enabled = multipart_uploads_enabled
155156

156157
if graphiql is not None:
157158
warnings.warn(
@@ -229,7 +230,6 @@ def get_context(self, request: HttpRequest, response: HttpResponse) -> Any:
229230
def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse:
230231
return TemporalHttpResponse()
231232

232-
@method_decorator(csrf_exempt)
233233
def dispatch(
234234
self, request: HttpRequest, *args: Any, **kwargs: Any
235235
) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponseBase]:
@@ -288,7 +288,6 @@ async def get_context(self, request: HttpRequest, response: HttpResponse) -> Any
288288
async def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse:
289289
return TemporalHttpResponse()
290290

291-
@method_decorator(csrf_exempt)
292291
async def dispatch( # pyright: ignore
293292
self, request: HttpRequest, *args: Any, **kwargs: Any
294293
) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponseBase]:

strawberry/fastapi/router.py

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def __init__(
156156
generate_unique_id_function: Callable[[APIRoute], str] = Default(
157157
generate_unique_id
158158
),
159+
multipart_uploads_enabled: bool = False,
159160
**kwargs: Any,
160161
) -> None:
161162
super().__init__(
@@ -190,6 +191,7 @@ def __init__(
190191
)
191192
self.protocols = subscription_protocols
192193
self.connection_init_wait_timeout = connection_init_wait_timeout
194+
self.multipart_uploads_enabled = multipart_uploads_enabled
193195

194196
if graphiql is not None:
195197
warnings.warn(

strawberry/flask/views.py

+2
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,12 @@ def __init__(
7171
graphiql: Optional[bool] = None,
7272
graphql_ide: Optional[GraphQL_IDE] = "graphiql",
7373
allow_queries_via_get: bool = True,
74+
multipart_uploads_enabled: bool = False,
7475
) -> None:
7576
self.schema = schema
7677
self.graphiql = graphiql
7778
self.allow_queries_via_get = allow_queries_via_get
79+
self.multipart_uploads_enabled = multipart_uploads_enabled
7880

7981
if graphiql is not None:
8082
warnings.warn(

strawberry/http/async_base_view.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ async def parse_http_body(
333333
data = self.parse_query_params(request.query_params)
334334
elif "application/json" in content_type:
335335
data = self.parse_json(await request.get_body())
336-
elif content_type == "multipart/form-data":
336+
elif self.multipart_uploads_enabled and content_type == "multipart/form-data":
337337
data = await self.parse_multipart(request)
338338
else:
339339
raise HTTPException(400, "Unsupported content type")

strawberry/http/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def headers(self) -> Mapping[str, str]: ...
2323

2424
class BaseView(Generic[Request]):
2525
graphql_ide: Optional[GraphQL_IDE]
26+
multipart_uploads_enabled: bool = False
2627

2728
# TODO: we might remove this in future :)
2829
_ide_replace_variables: bool = True

0 commit comments

Comments
 (0)