diff --git a/README.md b/README.md
index 7f14bf3..74844fb 100644
--- a/README.md
+++ b/README.md
@@ -549,6 +549,7 @@ endpoint.
| `MaximumFileSize` | Sets the maximum file size allowed for GraphQL multipart requests. | unlimited |
| `MaximumFileCount` | Sets the maximum number of files allowed for GraphQL multipart requests. | unlimited |
| `ReadExtensionsFromQueryString` | Enables reading extensions from the query string. | True |
+| `ReadFormOnPost` | Enables parsing of form data for POST requests (may have security implications). | True |
| `ReadQueryStringOnPost` | Enables parsing the query string on POST requests. | True |
| `ReadVariablesFromQueryString` | Enables reading variables from the query string. | True |
| `ValidationErrorsReturnBadRequest` | When enabled, GraphQL requests with validation errors have the HTTP status code set to 400 Bad Request. | True |
@@ -758,6 +759,27 @@ are rejected over HTTP GET connections. Derive from `GraphQLHttpMiddleware` and
As would be expected, subscription requests are only allowed over WebSocket channels.
+### Handling form data for POST requests
+
+The GraphQL over HTTP specification does not outline a procedure for transmitting GraphQL requests via
+HTTP POST connections using a `Content-Type` of `application/x-www-form-urlencoded` or `multipart/form-data`.
+Allowing the processing of such requests could be advantageous in avoiding CORS preflight requests when
+sending GraphQL queries from a web browser. Nevertheless, enabling this feature may give rise to security
+risks when utilizing cookie authentication, since transmitting cookies with these requests does not trigger
+a pre-flight CORS check. As a consequence, GraphQL.NET might execute a request and potentially modify data
+even when the CORS policy prohibits it, regardless of whether the sender has access to the response.
+This situation exposes the system to security vulnerabilities, which should be carefully evaluated and
+mitigated to ensure the safe handling of GraphQL requests and maintain the integrity of the data.
+
+This functionality is activated by default to maintain backward compatibility, but it can be turned off by
+setting the `ReadFormOnPost` value to `false`. The next major version of GraphQL.NET Server will have this
+feature disabled by default, enhancing security measures.
+
+Keep in mind that CORS pre-flight requests are also not executed for GET requests, potentially presenting a
+security risk. However, GraphQL query operations usually do not alter data, and mutations are refused.
+Additionally, the response is not expected to be readable in the browser (unless CORS checks are successful),
+which helps alleviate this concern.
+
### Excessive `OperationCanceledException`s
When hosting a WebSockets endpoint, it may be common for clients to simply disconnect rather
diff --git a/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs b/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs
index 90d1642..cf79386 100644
--- a/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs
+++ b/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs
@@ -223,7 +223,7 @@ public virtual async Task InvokeAsync(HttpContext context)
return (await DeserializeFromGraphBodyAsync(httpRequest.Body, sourceEncoding), null);
default:
- if (httpRequest.HasFormContentType) {
+ if (httpRequest.HasFormContentType && _options.ReadFormOnPost) {
try {
var formCollection = await httpRequest.ReadFormAsync(context.RequestAborted);
return ReadFormContent(formCollection);
@@ -795,7 +795,13 @@ protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext c
/// Writes a '415 Invalid Content-Type header: non-supported media type.' message to the output.
///
protected virtual Task HandleInvalidContentTypeErrorAsync(HttpContext context, RequestDelegate next)
- => WriteErrorResponseAsync(context, HttpStatusCode.UnsupportedMediaType, new InvalidContentTypeError($"non-supported media type '{context.Request.ContentType}'. Must be '{MEDIATYPE_JSON}', '{MEDIATYPE_GRAPHQL}' or a form body."));
+ => WriteErrorResponseAsync(
+ context,
+ HttpStatusCode.UnsupportedMediaType,
+ _options.ReadFormOnPost
+ ? new InvalidContentTypeError($"non-supported media type '{context.Request.ContentType}'. Must be '{MEDIATYPE_JSON}', '{MEDIATYPE_GRAPHQL}' or a form body.")
+ : new InvalidContentTypeError($"non-supported media type '{context.Request.ContentType}'. Must be '{MEDIATYPE_JSON}' or '{MEDIATYPE_GRAPHQL}'.")
+ );
///
/// Indicates that an unsupported HTTP method was requested.
diff --git a/src/GraphQL.AspNetCore3/GraphQLHttpMiddlewareOptions.cs b/src/GraphQL.AspNetCore3/GraphQLHttpMiddlewareOptions.cs
index dc461c0..ab07f17 100644
--- a/src/GraphQL.AspNetCore3/GraphQLHttpMiddlewareOptions.cs
+++ b/src/GraphQL.AspNetCore3/GraphQLHttpMiddlewareOptions.cs
@@ -48,6 +48,19 @@ public class GraphQLHttpMiddlewareOptions : IAuthorizationOptions
///
public bool ReadQueryStringOnPost { get; set; } = true;
+ ///
+ /// Enables parsing POST requests with the form content types such as multipart-form/data.
+ ///
+ ///
+ /// There is a potential security vulnerability when employing cookie authentication
+ /// with the multipart-form/data content type because sending cookies
+ /// alongside the request does not initiate a pre-flight CORS request.
+ /// As a result, GraphQL.NET carries out the request and potentially modifies data,
+ /// even if the CORS policy forbids it, irrespective of the sender's ability to access
+ /// the response.
+ ///
+ public bool ReadFormOnPost { get; set; } = true;
+
///
/// Enables reading variables from the query string.
/// Variables are interpreted as JSON and deserialized before being
diff --git a/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt b/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt
index 68eee28..d009bee 100644
--- a/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt
+++ b/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt
@@ -166,6 +166,7 @@ namespace GraphQL.AspNetCore3
public int? MaximumFileCount { get; set; }
public long? MaximumFileSize { get; set; }
public bool ReadExtensionsFromQueryString { get; set; }
+ public bool ReadFormOnPost { get; set; }
public bool ReadQueryStringOnPost { get; set; }
public bool ReadVariablesFromQueryString { get; set; }
public bool ValidationErrorsReturnBadRequest { get; set; }
diff --git a/src/Tests/Middleware/PostTests.cs b/src/Tests/Middleware/PostTests.cs
index 8c22307..d19fa8f 100644
--- a/src/Tests/Middleware/PostTests.cs
+++ b/src/Tests/Middleware/PostTests.cs
@@ -423,18 +423,31 @@ public async Task ContentType_GraphQLJson(string contentType)
}
[Theory]
- [InlineData(false)]
- [InlineData(true)]
- public async Task UnknownContentType(bool badRequest)
+ [InlineData(false, true, "application/pdf")]
+ [InlineData(true, true, "application/pdf")]
+ [InlineData(false, false, "multipart-form/data")]
+ [InlineData(true, false, "multipart-form/data")]
+ [InlineData(false, false, "application/x-www-form-urlencoded")]
+ [InlineData(true, false, "application/x-www-form-urlencoded")]
+ public async Task UnknownContentType(bool badRequest, bool allowFormBody, string contentType)
{
_options.ValidationErrorsReturnBadRequest = badRequest;
+ _options.ReadFormOnPost = allowFormBody;
var client = _server.CreateClient();
- var content = new StringContent("{count}");
- content.Headers.ContentType = new("application/pdf");
+ HttpContent content = contentType switch {
+ "application/pdf" => new StringContent("{count}", null, contentType),
+ "multipart-form/data" => new MultipartFormDataContent { { new StringContent("{count}", null, "application/graphql"), "query" } },
+ "application/x-www-form-urlencoded" => new FormUrlEncodedContent(new[] { new KeyValuePair("query", "{count}") }),
+ _ => throw new ArgumentOutOfRangeException(nameof(contentType))
+ };
using var response = await client.PostAsync("/graphql", content);
response.StatusCode.ShouldBe(HttpStatusCode.UnsupportedMediaType);
var actual = await response.Content.ReadAsStringAsync();
- actual.ShouldBe(@"{""errors"":[{""message"":""Invalid \u0027Content-Type\u0027 header: non-supported media type \u0027application/pdf\u0027. Must be \u0027application/json\u0027, \u0027application/graphql\u0027 or a form body."",""extensions"":{""code"":""INVALID_CONTENT_TYPE"",""codes"":[""INVALID_CONTENT_TYPE""]}}]}");
+ if (allowFormBody) {
+ actual.ShouldBe($@"{{""errors"":[{{""message"":""Invalid \u0027Content-Type\u0027 header: non-supported media type \u0027{content.Headers.ContentType?.ToString().Replace("\"", "\\u0022")}\u0027. Must be \u0027application/json\u0027, \u0027application/graphql\u0027 or a form body."",""extensions"":{{""code"":""INVALID_CONTENT_TYPE"",""codes"":[""INVALID_CONTENT_TYPE""]}}}}]}}");
+ } else {
+ actual.ShouldBe($@"{{""errors"":[{{""message"":""Invalid \u0027Content-Type\u0027 header: non-supported media type \u0027{content.Headers.ContentType?.ToString().Replace("\"", "\\u0022")}\u0027. Must be \u0027application/json\u0027 or \u0027application/graphql\u0027."",""extensions"":{{""code"":""INVALID_CONTENT_TYPE"",""codes"":[""INVALID_CONTENT_TYPE""]}}}}]}}");
+ }
}
[Theory]