VYPR
High severityNVD Advisory· Published Jun 9, 2021· Updated Aug 3, 2024

Cross-Site Request Forgery (CSRF) in FastAPI

CVE-2021-32677

Description

FastAPI <0.65.2 improperly parsed JSON from text/plain requests, enabling CSRF attacks by bypassing CORS preflight when cookies are used for authentication.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

FastAPI <0.65.2 improperly parsed JSON from text/plain requests, enabling CSRF attacks by bypassing CORS preflight when cookies are used for authentication.

Vulnerability

FastAPI versions prior to 0.65.2, when using cookie-based authentication in path operations that accept JSON payloads, would parse the request body as JSON regardless of the Content-Type header. A browser-sent request with Content-Type: text/plain containing valid JSON would be accepted and the JSON data extracted. This behavior allowed Cross-Site Request Forgery (CSRF) attacks because text/plain is classified as a "simple" request type, exempt from CORS preflight checks, and cookies are automatically attached. The flaw exists in the request body parsing logic and affects all FastAPI versions below 0.65.2 [1][2].

Exploitation

An attacker can craft a malicious web page that, when visited by an authenticated user, submits a form or uses JavaScript to send a POST request with Content-Type: text/plain containing a JSON payload that mimics a legitimate API operation. The browser automatically includes the victim's cookies for the target origin. Since the request appears as a simple request, no CORS preflight occurs, and FastAPI accepts the JSON body. The attacker must know the target endpoint and the expected JSON structure to perform the operation [2][3].

Impact

Successful exploitation allows an attacker to perform state-changing operations on behalf of an authenticated user without their consent. The impact can range from data modification to privilege escalation, depending on the affected endpoint. The attack leverages the user's existing session cookies, potentially compromising account integrity and confidentiality [2].

Mitigation

The vulnerability is fixed in FastAPI 0.65.2, released on 2021-06-09, which validates the Content-Type header and only parses JSON for application/json or JSON-compatible media types (e.g., application/geo+json). If upgrading is not immediately possible, a middleware or dependency that checks and rejects requests with non-JSON Content-Type headers (e.g., text/plain) can serve as a workaround [2][3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
fastapiPyPI
< 0.65.20.65.2

Affected products

2

Patches

1
fa7e3c996edf

🐛 Check Content-Type request header before assuming JSON (#2118)

https://github.com/tiangolo/fastapiPatrick WangJun 7, 2021via ghsa
3 files changed · +92 12
  • fastapi/routing.py+16 3 modified
    @@ -1,4 +1,5 @@
     import asyncio
    +import email.message
     import enum
     import inspect
     import json
    @@ -36,7 +37,7 @@
     )
     from pydantic import BaseModel
     from pydantic.error_wrappers import ErrorWrapper, ValidationError
    -from pydantic.fields import ModelField
    +from pydantic.fields import ModelField, Undefined
     from starlette import routing
     from starlette.concurrency import run_in_threadpool
     from starlette.exceptions import HTTPException
    @@ -174,14 +175,26 @@ def get_request_handler(
     
         async def app(request: Request) -> Response:
             try:
    -            body = None
    +            body: Any = None
                 if body_field:
                     if is_body_form:
                         body = await request.form()
                     else:
                         body_bytes = await request.body()
                         if body_bytes:
    -                        body = await request.json()
    +                        json_body: Any = Undefined
    +                        content_type_value = request.headers.get("content-type")
    +                        if content_type_value:
    +                            message = email.message.Message()
    +                            message["content-type"] = content_type_value
    +                            if message.get_content_maintype() == "application":
    +                                subtype = message.get_content_subtype()
    +                                if subtype == "json" or subtype.endswith("+json"):
    +                                    json_body = await request.json()
    +                        if json_body != Undefined:
    +                            body = json_body
    +                        else:
    +                            body = body_bytes
             except json.JSONDecodeError as e:
                 raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc)
             except Exception as e:
    
  • tests/test_tutorial/test_body/test_tutorial001.py+75 9 modified
    @@ -173,25 +173,91 @@ def test_post_body(path, body, expected_status, expected_response):
     
     
     def test_post_broken_body():
    -    response = client.post("/items/", data={"name": "Foo", "price": 50.5})
    +    response = client.post(
    +        "/items/",
    +        headers={"content-type": "application/json"},
    +        data="{some broken json}",
    +    )
         assert response.status_code == 422, response.text
         assert response.json() == {
             "detail": [
                 {
    +                "loc": ["body", 1],
    +                "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
    +                "type": "value_error.jsondecode",
                     "ctx": {
    -                    "colno": 1,
    -                    "doc": "name=Foo&price=50.5",
    +                    "msg": "Expecting property name enclosed in double quotes",
    +                    "doc": "{some broken json}",
    +                    "pos": 1,
                         "lineno": 1,
    -                    "msg": "Expecting value",
    -                    "pos": 0,
    +                    "colno": 2,
                     },
    -                "loc": ["body", 0],
    -                "msg": "Expecting value: line 1 column 1 (char 0)",
    -                "type": "value_error.jsondecode",
                 }
             ]
         }
    +
    +
    +def test_post_form_for_json():
    +    response = client.post("/items/", data={"name": "Foo", "price": 50.5})
    +    assert response.status_code == 422, response.text
    +    assert response.json() == {
    +        "detail": [
    +            {
    +                "loc": ["body"],
    +                "msg": "value is not a valid dict",
    +                "type": "type_error.dict",
    +            }
    +        ]
    +    }
    +
    +
    +def test_explicit_content_type():
    +    response = client.post(
    +        "/items/",
    +        data='{"name": "Foo", "price": 50.5}',
    +        headers={"Content-Type": "application/json"},
    +    )
    +    assert response.status_code == 200, response.text
    +
    +
    +def test_geo_json():
    +    response = client.post(
    +        "/items/",
    +        data='{"name": "Foo", "price": 50.5}',
    +        headers={"Content-Type": "application/geo+json"},
    +    )
    +    assert response.status_code == 200, response.text
    +
    +
    +def test_wrong_headers():
    +    data = '{"name": "Foo", "price": 50.5}'
    +    invalid_dict = {
    +        "detail": [
    +            {
    +                "loc": ["body"],
    +                "msg": "value is not a valid dict",
    +                "type": "type_error.dict",
    +            }
    +        ]
    +    }
    +
    +    response = client.post("/items/", data=data, headers={"Content-Type": "text/plain"})
    +    assert response.status_code == 422, response.text
    +    assert response.json() == invalid_dict
    +
    +    response = client.post(
    +        "/items/", data=data, headers={"Content-Type": "application/geo+json-seq"}
    +    )
    +    assert response.status_code == 422, response.text
    +    assert response.json() == invalid_dict
    +    response = client.post(
    +        "/items/", data=data, headers={"Content-Type": "application/not-really-json"}
    +    )
    +    assert response.status_code == 422, response.text
    +    assert response.json() == invalid_dict
    +
    +
    +def test_other_exceptions():
         with patch("json.loads", side_effect=Exception):
             response = client.post("/items/", json={"test": "test2"})
             assert response.status_code == 400, response.text
    -    assert response.json() == {"detail": "There was an error parsing the body"}
    
  • tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py+1 0 modified
    @@ -25,6 +25,7 @@ def test_gzip_request(compress):
         if compress:
             data = gzip.compress(data)
             headers["Content-Encoding"] = "gzip"
    +    headers["Content-Type"] = "application/json"
         response = client.post("/sum", data=data, headers=headers)
         assert response.json() == {"sum": n}
     
    

Vulnerability mechanics

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

References

7

News mentions

0

No linked articles in our index yet.