|
| 1 | +<p>This is an issue when a FastAPI endpoint accepts file uploads (using <code>File()</code>) and also receives structured data through |
| 2 | +<code>Body()</code> parameters or query parameters (via <code>Depends()</code> without proper form handling). This pattern causes either validation |
| 3 | +errors at runtime or exposes sensitive information in URLs and logs.</p> |
| 4 | +<h2>Why is this an issue?</h2> |
| 5 | +<p>When building web APIs with FastAPI, developers often need to create endpoints that accept both file uploads and structured data. However, the way |
| 6 | +this data is transmitted requires careful consideration due to HTTP protocol constraints and security implications.</p> |
| 7 | +<h2>Understanding HTTP Content Types</h2> |
| 8 | +<p>HTTP requests can encode data in different ways, specified by the <code>Content-Type</code> header:</p> |
| 9 | +<ul> |
| 10 | + <li> <code>application/json</code> - Used for JSON data in the request body </li> |
| 11 | + <li> <code>multipart/form-data</code> - Required for file uploads, encodes both files and form fields </li> |
| 12 | + <li> Query parameters - Appended to the URL after a <code>?</code> character </li> |
| 13 | +</ul> |
| 14 | +<p>When a FastAPI endpoint includes <code>File()</code> parameters, the client must send the request using <code>multipart/form-data</code> encoding. |
| 15 | +This creates a fundamental incompatibility: you cannot mix <code>Body()</code> parameters (which expect <code>application/json</code>) with |
| 16 | +<code>File()</code> parameters in the same endpoint.</p> |
| 17 | +<h2>The Technical Problem</h2> |
| 18 | +<p>If you declare both <code>Body()</code> and <code>File()</code> parameters in the same endpoint, FastAPI cannot properly parse the request. The |
| 19 | +framework expects JSON in the body when it sees <code>Body()</code> parameters, but receives form-encoded data instead when files are included. This |
| 20 | +results in validation errors like "value is not a valid dict" or similar type mismatches.</p> |
| 21 | +<h2>The Security Problem</h2> |
| 22 | +<p>Some developers work around the technical constraint by passing structured data through query parameters using <code>Depends()</code> with a |
| 23 | +Pydantic model. While this avoids the encoding conflict, it creates a serious security vulnerability.</p> |
| 24 | +<p>Query parameters appear in the URL itself, which means they are:</p> |
| 25 | +<ul> |
| 26 | + <li> Visible in browser address bars </li> |
| 27 | + <li> Stored in browser history </li> |
| 28 | + <li> Logged in web server access logs </li> |
| 29 | + <li> Potentially cached by proxies and CDNs </li> |
| 30 | + <li> Visible in network monitoring tools </li> |
| 31 | +</ul> |
| 32 | +<p>If the structured data contains sensitive information (user credentials, personal data, tokens, etc.), this exposure creates a significant security |
| 33 | +risk. An attacker with access to server logs, browser history, or network traffic can extract this sensitive information.</p> |
| 34 | +<h2>Why Form Data is the Solution</h2> |
| 35 | +<p>Form data transmitted via <code>multipart/form-data</code> encoding:</p> |
| 36 | +<ul> |
| 37 | + <li> Is compatible with file uploads </li> |
| 38 | + <li> Is sent in the request body, not the URL </li> |
| 39 | + <li> Does not appear in server logs (only the URL path is logged) </li> |
| 40 | + <li> Can be properly validated and parsed by FastAPI </li> |
| 41 | +</ul> |
| 42 | +<p>The challenge is that form data is transmitted as strings, not as structured JSON objects. This is where Pydantic’s custom validators become |
| 43 | +essential - they allow you to parse JSON strings from form fields while maintaining type safety and validation.</p> |
| 44 | +<h3>What is the potential impact?</h3> |
| 45 | +<h3>Exposure of Sensitive Data</h3> |
| 46 | +<p>When structured data containing sensitive information is passed through query parameters, it becomes visible in multiple locations:</p> |
| 47 | +<ul> |
| 48 | + <li> Server access logs permanently record the full URL including all query parameters </li> |
| 49 | + <li> Browser history stores the complete URL, accessible to anyone with access to the device </li> |
| 50 | + <li> Network intermediaries (proxies, load balancers) may log or cache the URLs </li> |
| 51 | + <li> Referrer headers may leak the URL to third-party sites </li> |
| 52 | +</ul> |
| 53 | +<p>This exposure can lead to unauthorized access to user accounts, personal information disclosure, or compliance violations (GDPR, HIPAA, |
| 54 | +PCI-DSS).</p> |
| 55 | +<h3>Application Failures</h3> |
| 56 | +<p>When <code>Body()</code> and <code>File()</code> parameters are mixed incorrectly, the application will fail at runtime with validation errors. |
| 57 | +Users will be unable to complete file upload operations, resulting in:</p> |
| 58 | +<ul> |
| 59 | + <li> Poor user experience </li> |
| 60 | + <li> Failed business processes </li> |
| 61 | + <li> Support burden from confused users </li> |
| 62 | + <li> Potential data loss if users abandon the operation </li> |
| 63 | +</ul> |
| 64 | +<h2>How to fix it in FastAPI</h2> |
| 65 | +<p>Replace <code>Body()</code> parameters with <code>Form()</code> parameters and add a Pydantic validator to parse JSON strings. This ensures |
| 66 | +compatibility with file uploads while maintaining type safety.</p> |
| 67 | +<h3>Code examples</h3> |
| 68 | +<h4>Noncompliant code example</h4> |
| 69 | +<pre data-diff-id="1" data-diff-type="noncompliant"> |
| 70 | +@router.post("/upload") |
| 71 | +async def create_policy( |
| 72 | + countryId: str = Body(...), # Noncompliant |
| 73 | + policyDetails: List[dict] = Body(...), # Noncompliant |
| 74 | + files: List[UploadFile] = File(...) |
| 75 | +): |
| 76 | + return {"status": "ok"} |
| 77 | +</pre> |
| 78 | +<h4>Compliant solution</h4> |
| 79 | +<pre data-diff-id="1" data-diff-type="compliant"> |
| 80 | +class PolicyData(BaseModel): |
| 81 | + countryId: str |
| 82 | + policyDetails: List[dict] |
| 83 | + |
| 84 | + @model_validator(mode='before') |
| 85 | + @classmethod |
| 86 | + def validate_to_json(cls, value): |
| 87 | + if isinstance(value, str): |
| 88 | + return cls(**json.loads(value)) |
| 89 | + return value |
| 90 | + |
| 91 | +@router.post("/upload") |
| 92 | +async def create_policy( |
| 93 | + data: PolicyData = Form(...), |
| 94 | + files: List[UploadFile] = File(...) |
| 95 | +): |
| 96 | + return {"status": "ok"} |
| 97 | +</pre> |
| 98 | +<p>Replace <code>Depends()</code> with <code>Form()</code> when passing structured data alongside file uploads. Use a custom validator to parse the |
| 99 | +JSON string from the form field.</p> |
| 100 | +<h4>Noncompliant code example</h4> |
| 101 | +<pre data-diff-id="2" data-diff-type="noncompliant"> |
| 102 | +@app.post("/submit") |
| 103 | +def submit( |
| 104 | + base: Base = Depends(), # Noncompliant - data exposed in query parameters |
| 105 | + files: List[UploadFile] = File(...) |
| 106 | +): |
| 107 | + return {"JSON Payload": base, "Filenames": [file.filename for file in files]} |
| 108 | +</pre> |
| 109 | +<h4>Compliant solution</h4> |
| 110 | +<pre data-diff-id="2" data-diff-type="compliant"> |
| 111 | +class Base(BaseModel): |
| 112 | + countryId: str |
| 113 | + sensitiveData: str |
| 114 | + |
| 115 | + @model_validator(mode='before') |
| 116 | + @classmethod |
| 117 | + def validate_to_json(cls, value): |
| 118 | + if isinstance(value, str): |
| 119 | + return cls(**json.loads(value)) |
| 120 | + return value |
| 121 | + |
| 122 | +@app.post("/submit") |
| 123 | +def submit( |
| 124 | + base: Base = Form(...), |
| 125 | + files: List[UploadFile] = File(...) |
| 126 | +): |
| 127 | + return {"JSON Payload": base, "Filenames": [file.filename for file in files]} |
| 128 | +</pre> |
| 129 | +<p>If you need to accept complex nested structures, create a dependency function that reads from <code>Form()</code> and performs validation with |
| 130 | +proper error handling.</p> |
| 131 | +<h4>Noncompliant code example</h4> |
| 132 | +<pre data-diff-id="3" data-diff-type="noncompliant"> |
| 133 | +@app.post("/data") |
| 134 | +async def upload_data( |
| 135 | + config: DataConfiguration = Depends(), # Noncompliant |
| 136 | + csvFile: UploadFile = File(...) |
| 137 | +): |
| 138 | + pass |
| 139 | +</pre> |
| 140 | +<h4>Compliant solution</h4> |
| 141 | +<pre data-diff-id="3" data-diff-type="compliant"> |
| 142 | +from fastapi import HTTPException, status |
| 143 | +from fastapi.encoders import jsonable_encoder |
| 144 | +from pydantic import ValidationError |
| 145 | + |
| 146 | +def parse_config(data: str = Form(...)) -> DataConfiguration: |
| 147 | + try: |
| 148 | + return DataConfiguration.model_validate_json(data) |
| 149 | + except ValidationError as e: |
| 150 | + raise HTTPException( |
| 151 | + detail=jsonable_encoder(e.errors()), |
| 152 | + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, |
| 153 | + ) |
| 154 | + |
| 155 | +@app.post("/data") |
| 156 | +async def upload_data( |
| 157 | + config: DataConfiguration = Depends(parse_config), |
| 158 | + csvFile: UploadFile = File(...) |
| 159 | +): |
| 160 | + pass |
| 161 | +</pre> |
| 162 | +<h2>Resources</h2> |
| 163 | +<h3>Documentation</h3> |
| 164 | +<ul> |
| 165 | + <li> FastAPI - Request Forms and Files - <a href="https://fastapi.tiangolo.com/tutorial/request-forms-and-files/">Official FastAPI documentation on |
| 166 | + handling forms and files together</a> </li> |
| 167 | + <li> FastAPI - Request Body - <a href="https://fastapi.tiangolo.com/tutorial/body/">Official documentation explaining Body parameters and JSON |
| 168 | + encoding</a> </li> |
| 169 | + <li> Pydantic - Validators - <a href="https://docs.pydantic.dev/latest/concepts/validators/">Documentation on Pydantic validators for custom data |
| 170 | + parsing</a> </li> |
| 171 | + <li> OWASP - Logging Cheat Sheet - <a href="https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html">Guidelines on what data should |
| 172 | + not be logged, including query parameters with sensitive data</a> </li> |
| 173 | +</ul> |
| 174 | +<h3>Standards</h3> |
| 175 | +<ul> |
| 176 | + <li> OWASP Top 10 2021 A01 - <a href="https://owasp.org/Top10/A01_2021-Broken_Access_Control/">Broken Access Control - exposing sensitive data in |
| 177 | + URLs can lead to unauthorized access</a> </li> |
| 178 | + <li> OWASP Top 10 2021 A09 - <a href="https://owasp.org/Top10/A09_2021-Security_Logging_and_Monitoring_Failures/">Security Logging and Monitoring |
| 179 | + Failures - sensitive data in logs creates security risks</a> </li> |
| 180 | + <li> CWE 359 - <a href="https://cwe.mitre.org/data/definitions/359.html">Exposure of Private Personal Information to an Unauthorized Actor</a> </li> |
| 181 | + <li> CWE 598 - <a href="https://cwe.mitre.org/data/definitions/598.html">Use of GET Request Method With Sensitive Query Strings</a> </li> |
| 182 | +</ul> |
| 183 | + |
0 commit comments