Skip to content

Commit f7a56aa

Browse files
emanndbanty
andauthored
Merge pull request from GHSA-9x4c-63pf-525f
* All strings used as file/directory names are now sanitized to address the path traversal vulnerabilities * Switched calls to utils.spinal_case to utils.kebab_case * Quotation marks are now escaped in all names/descriptions to address arbitrary code execution issues * Sanitized identifiers are now also checked for keyword collision * Added validation for default property values where possible Address arbitrary code execution vulnerability * Enum keys are now also sanitized * Update end_to_end_tests/fastapi_app/__init__.py Co-authored-by: Dylan Anthony <[email protected]> * Update openapi_python_client/parser/properties.py Co-authored-by: Dylan Anthony <[email protected]> * Changed NotImplementedErrors to ValidationErrors * Updated changelog * Fixed regex * Update openapi_python_client/utils.py Co-authored-by: Dylan Anthony <[email protected]> * Fixed rendering of union, datetime, and date properties. Added e2e test for all defaults * Fixed rendering of date/datetime default values when timezone is included * Simplified datetime rendering * Fixed typo * Readded support for DictProperty defaults and fixed a bug with json body template rendering * Fixed dict json body compatibility Co-authored-by: Ethan Mann <[email protected]> Co-authored-by: Dylan Anthony <[email protected]>
1 parent 3e7dfae commit f7a56aa

File tree

19 files changed

+744
-98
lines changed

19 files changed

+744
-98
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77

8-
## 0.5.3 - Unrelease
8+
## 0.5.3 - Unreleased
9+
### Security
10+
- All values that become file/directory names are sanitized to address path traversal vulnerabilities (CVE-2020-15141)
11+
- All values that get placed into python files (everything from enum names, to endpoint descriptions, to default values) are validated and/or saniziatied to address arbitrary code execution vulnerabilities (CVE-2020-15142)
12+
13+
### Changes
14+
- Due to security concerns/implementation complexities, default values are temporarily unsupported for any `RefProperty` that doesn't refer to an enum.
15+
- Defaults for properties must now be valid values for their respective type (e.g. "example string" is an invalid default for an `integer` type property, and the function for an endpoint using it would fail to generate and be skipped).
16+
917
### Additions
1018
- Added support for header parameters (#117)
1119

end_to_end_tests/fastapi_app/__init__.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pathlib import Path
66
from typing import Any, Dict, List, Union
77

8-
from fastapi import APIRouter, FastAPI, File, Header, Query, UploadFile
8+
from fastapi import APIRouter, Body, FastAPI, File, Header, Query, UploadFile
99
from pydantic import BaseModel
1010

1111
app = FastAPI(title="My Test API", description="An API for testing openapi-python-client",)
@@ -43,13 +43,15 @@ class AModel(BaseModel):
4343

4444
an_enum_value: AnEnum
4545
nested_list_of_enums: List[List[DifferentEnum]] = []
46-
some_dict: Dict[str, str] = {}
46+
some_dict: Dict[str, str]
4747
aCamelDateTime: Union[datetime, date]
4848
a_date: date
4949

5050

5151
@test_router.get("/", response_model=List[AModel], operation_id="getUserList")
52-
def get_list(an_enum_value: List[AnEnum] = Query(...), some_date: Union[date, datetime] = Query(...)):
52+
def get_list(
53+
an_enum_value: List[AnEnum] = Query(...), some_date: Union[date, datetime] = Query(...),
54+
):
5355
""" Get a list of things """
5456
return
5557

@@ -67,6 +69,22 @@ def json_body(body: AModel):
6769
return
6870

6971

72+
@test_router.post("/test_defaults")
73+
def test_defaults(
74+
string_prop: str = Query(default="the default string"),
75+
datetime_prop: datetime = Query(default=datetime(1010, 10, 10)),
76+
date_prop: date = Query(default=date(1010, 10, 10)),
77+
float_prop: float = Query(default=3.14),
78+
int_prop: int = Query(default=7),
79+
boolean_prop: bool = Query(default=False),
80+
list_prop: List[AnEnum] = Query(default=[AnEnum.FIRST_VALUE, AnEnum.SECOND_VALUE]),
81+
union_prop: Union[float, str] = Query(default="not a float"),
82+
enum_prop: AnEnum = Query(default=AnEnum.FIRST_VALUE),
83+
dict_prop: Dict[str, str] = Body(default={"key": "val"}),
84+
):
85+
return
86+
87+
7088
app.include_router(test_router, prefix="/tests", tags=["tests"])
7189

7290

end_to_end_tests/fastapi_app/openapi.json

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,156 @@
184184
}
185185
}
186186
}
187+
},
188+
"/tests/test_defaults": {
189+
"post": {
190+
"tags": [
191+
"tests"
192+
],
193+
"summary": "Test Defaults",
194+
"operationId": "test_defaults_tests_test_defaults_post",
195+
"parameters": [
196+
{
197+
"required": false,
198+
"schema": {
199+
"title": "String Prop",
200+
"type": "string",
201+
"default": "the default string"
202+
},
203+
"name": "string_prop",
204+
"in": "query"
205+
},
206+
{
207+
"required": false,
208+
"schema": {
209+
"title": "Datetime Prop",
210+
"type": "string",
211+
"format": "date-time",
212+
"default": "1010-10-10T00:00:00"
213+
},
214+
"name": "datetime_prop",
215+
"in": "query"
216+
},
217+
{
218+
"required": false,
219+
"schema": {
220+
"title": "Date Prop",
221+
"type": "string",
222+
"format": "date",
223+
"default": "1010-10-10"
224+
},
225+
"name": "date_prop",
226+
"in": "query"
227+
},
228+
{
229+
"required": false,
230+
"schema": {
231+
"title": "Float Prop",
232+
"type": "number",
233+
"default": 3.14
234+
},
235+
"name": "float_prop",
236+
"in": "query"
237+
},
238+
{
239+
"required": false,
240+
"schema": {
241+
"title": "Int Prop",
242+
"type": "integer",
243+
"default": 7
244+
},
245+
"name": "int_prop",
246+
"in": "query"
247+
},
248+
{
249+
"required": false,
250+
"schema": {
251+
"title": "Boolean Prop",
252+
"type": "boolean",
253+
"default": false
254+
},
255+
"name": "boolean_prop",
256+
"in": "query"
257+
},
258+
{
259+
"required": false,
260+
"schema": {
261+
"title": "List Prop",
262+
"type": "array",
263+
"items": {
264+
"$ref": "#/components/schemas/AnEnum"
265+
},
266+
"default": [
267+
"FIRST_VALUE",
268+
"SECOND_VALUE"
269+
]
270+
},
271+
"name": "list_prop",
272+
"in": "query"
273+
},
274+
{
275+
"required": false,
276+
"schema": {
277+
"title": "Union Prop",
278+
"anyOf": [
279+
{
280+
"type": "number"
281+
},
282+
{
283+
"type": "string"
284+
}
285+
],
286+
"default": "not a float"
287+
},
288+
"name": "union_prop",
289+
"in": "query"
290+
},
291+
{
292+
"required": false,
293+
"schema": {
294+
"$ref": "#/components/schemas/AnEnum"
295+
},
296+
"name": "enum_prop",
297+
"in": "query"
298+
}
299+
],
300+
"requestBody": {
301+
"content": {
302+
"application/json": {
303+
"schema": {
304+
"title": "Dict Prop",
305+
"type": "object",
306+
"additionalProperties": {
307+
"type": "string"
308+
},
309+
"default": {
310+
"key": "val"
311+
}
312+
}
313+
}
314+
}
315+
},
316+
"responses": {
317+
"200": {
318+
"description": "Successful Response",
319+
"content": {
320+
"application/json": {
321+
"schema": {}
322+
}
323+
}
324+
},
325+
"422": {
326+
"description": "Validation Error",
327+
"content": {
328+
"application/json": {
329+
"schema": {
330+
"$ref": "#/components/schemas/HTTPValidationError"
331+
}
332+
}
333+
}
334+
}
335+
}
336+
}
187337
}
188338
},
189339
"components": {
@@ -192,6 +342,7 @@
192342
"title": "AModel",
193343
"required": [
194344
"an_enum_value",
345+
"some_dict",
195346
"aCamelDateTime",
196347
"a_date"
197348
],
@@ -216,8 +367,7 @@
216367
"type": "object",
217368
"additionalProperties": {
218369
"type": "string"
219-
},
220-
"default": {}
370+
}
221371
},
222372
"aCamelDateTime": {
223373
"title": "Acameldatetime",

end_to_end_tests/golden-master/my_test_api_client/api/tests.py

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from dataclasses import asdict
2-
from datetime import date, datetime
1+
import datetime
2+
from dataclasses import asdict, field
33
from typing import Any, Dict, List, Optional, Union, cast
44

55
import httpx
@@ -13,7 +13,7 @@
1313

1414

1515
def get_user_list(
16-
*, client: Client, an_enum_value: List[AnEnum], some_date: Union[date, datetime],
16+
*, client: Client, an_enum_value: List[AnEnum], some_date: Union[datetime.date, datetime.datetime],
1717
) -> Union[
1818
List[AModel], HTTPValidationError,
1919
]:
@@ -29,7 +29,7 @@ def get_user_list(
2929

3030
json_an_enum_value.append(an_enum_value_item)
3131

32-
if isinstance(some_date, date):
32+
if isinstance(some_date, datetime.date):
3333
json_some_date = some_date.isoformat()
3434

3535
else:
@@ -94,3 +94,81 @@ def json_body_tests_json_body_post(
9494
return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
9595
else:
9696
raise ApiResponseError(response=response)
97+
98+
99+
def test_defaults_tests_test_defaults_post(
100+
*,
101+
client: Client,
102+
json_body: Dict[Any, Any],
103+
string_prop: Optional[str] = "the default string",
104+
datetime_prop: Optional[datetime.datetime] = datetime.datetime(1010, 10, 10, 0, 0),
105+
date_prop: Optional[datetime.date] = datetime.date(1010, 10, 10),
106+
float_prop: Optional[float] = 3.14,
107+
int_prop: Optional[int] = 7,
108+
boolean_prop: Optional[bool] = False,
109+
list_prop: Optional[List[AnEnum]] = field(
110+
default_factory=lambda: cast(Optional[List[AnEnum]], [AnEnum.FIRST_VALUE, AnEnum.SECOND_VALUE])
111+
),
112+
union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float",
113+
enum_prop: Optional[AnEnum] = None,
114+
) -> Union[
115+
None, HTTPValidationError,
116+
]:
117+
118+
""" """
119+
url = "{}/tests/test_defaults".format(client.base_url)
120+
121+
headers: Dict[str, Any] = client.get_headers()
122+
123+
json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None
124+
125+
json_date_prop = date_prop.isoformat() if date_prop else None
126+
127+
if list_prop is None:
128+
json_list_prop = None
129+
else:
130+
json_list_prop = []
131+
for list_prop_item_data in list_prop:
132+
list_prop_item = list_prop_item_data.value
133+
134+
json_list_prop.append(list_prop_item)
135+
136+
if union_prop is None:
137+
json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None
138+
elif isinstance(union_prop, float):
139+
json_union_prop = union_prop
140+
else:
141+
json_union_prop = union_prop
142+
143+
json_enum_prop = enum_prop.value if enum_prop else None
144+
145+
params: Dict[str, Any] = {}
146+
if string_prop is not None:
147+
params["string_prop"] = string_prop
148+
if datetime_prop is not None:
149+
params["datetime_prop"] = json_datetime_prop
150+
if date_prop is not None:
151+
params["date_prop"] = json_date_prop
152+
if float_prop is not None:
153+
params["float_prop"] = float_prop
154+
if int_prop is not None:
155+
params["int_prop"] = int_prop
156+
if boolean_prop is not None:
157+
params["boolean_prop"] = boolean_prop
158+
if list_prop is not None:
159+
params["list_prop"] = json_list_prop
160+
if union_prop is not None:
161+
params["union_prop"] = json_union_prop
162+
if enum_prop is not None:
163+
params["enum_prop"] = json_enum_prop
164+
165+
json_json_body = json_body
166+
167+
response = httpx.post(url=url, headers=headers, json=json_json_body, params=params,)
168+
169+
if response.status_code == 200:
170+
return None
171+
if response.status_code == 422:
172+
return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
173+
else:
174+
raise ApiResponseError(response=response)

0 commit comments

Comments
 (0)