CVE-2020-15141 (published 2020-08-14T17:15Z)

In openapi-python-client before version 0.5.3, there is a path traversal vulnerability. If a user generated a client using a maliciously crafted OpenAPI document, it is possible for generated files to be placed in arbitrary locations on disk.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:P/A:N)
CVSS v3 base score: 4.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/triaxtec/openapi-python-client
commit_details: repo_url: https://github.com/triaxtec/openapi-python-client
author: Dylan Anthony
author_date: 2020-08-13 12:57:06-04:00
msg: Merge pull request from GHSA-7wgr-7666-7pwj

* 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

Co-authored-by: Ethan Mann <emann@triaxtec.com>
repository: repo_url: https://github.com/triaxtec/openapi-python-client
repo_name: openapi-generators/openapi-python-client
description: Generate modern Python clients from OpenAPI
repo_language: Python
owner: triaxtec
file_changes: filename: regen_golden_master.py
change_type: ModificationType.MODIFY
diff: @@ -7,9 +7,6 @@ from typer.testing import CliRunner
 from openapi_python_client.cli import app
 
 if __name__ == "__main__":
-    from .fastapi_app import generate_openapi_json
-
-    generate_openapi_json()
     runner = CliRunner()
     openapi_path = Path(__file__).parent / "fastapi_app" / "openapi.json"
     gm_path = Path(__file__).parent / "golden-master"

programming_language: Python
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -83,7 +83,7 @@ def _get_document(*, url: Optional[str], path: Optional[Path]) -> Union[Dict[str
 
 
 class Project:
-    TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "spinalcase": utils.spinal_case}
+    TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "kebabcase": utils.kebab_case}
     project_name_override: Optional[str] = None
     package_name_override: Optional[str] = None
 
@@ -91,7 +91,7 @@ class Project:
         self.openapi: GeneratorData = openapi
         self.env: Environment = Environment(loader=PackageLoader(__package__), trim_blocks=True, lstrip_blocks=True)
 
-        self.project_name: str = self.project_name_override or f"{openapi.title.replace(' ', '-').lower()}-client"
+        self.project_name: str = self.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client"
         self.project_dir: Path = Path.cwd() / self.project_name
 
         self.package_name: str = self.package_name_override or self.project_name.replace("-", "_")
@@ -231,6 +231,7 @@ class Project:
         endpoint_template = self.env.get_template("endpoint_module.pyi")
         async_endpoint_template = self.env.get_template("async_endpoint_module.pyi")
         for tag, collection in self.openapi.endpoint_collections_by_tag.items():
+            tag = utils.snake_case(tag)
             module_path = api_dir / f"{tag}.py"
             module_path.write_text(endpoint_template.render(collection=collection))
             async_module_path = async_api_dir / f"{tag}.py"

programming_language: Python
filename: endpoint_macros.pyi
change_type: ModificationType.MODIFY
diff: @@ -2,10 +2,10 @@
 {% if endpoint.header_parameters %}
     {% for parameter in endpoint.header_parameters %}
         {% if parameter.required %}
-headers["{{ parameter.python_name | spinalcase}}"] = {{ parameter.python_name }}
+headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }}
         {% else %}
 if {{ parameter.python_name }} is not None:
-    headers["{{ parameter.python_name | spinalcase}}"] = {{ parameter.python_name }}
+    headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }}
         {% endif %}
     {% endfor %}
 {% endif %}

programming_language: HTML
filename: utils.py
change_type: ModificationType.MODIFY
diff: @@ -3,15 +3,23 @@ import re
 import stringcase
 
 
-def snake_case(value: str) -> str:
+def _sanitize(value: str) -> str:
+    return re.sub(r"[^\w _-]+", "", value)
+
+
+def group_title(value: str) -> str:
     value = re.sub(r"([A-Z]{2,})([A-Z][a-z]|[ -_]|$)", lambda m: m.group(1).title() + m.group(2), value.strip())
     value = re.sub(r"(^|[ _-])([A-Z])", lambda m: m.group(1) + m.group(2).lower(), value)
-    return stringcase.snakecase(value)
+    return value
+
+
+def snake_case(value: str) -> str:
+    return stringcase.snakecase(group_title(_sanitize(value)))
 
 
 def pascal_case(value: str) -> str:
-    return stringcase.pascalcase(value)
+    return stringcase.pascalcase(_sanitize(value))
 
 
-def spinal_case(value: str) -> str:
-    return stringcase.spinalcase(value)
+def kebab_case(value: str) -> str:
+    return stringcase.spinalcase(group_title(_sanitize(value)))

programming_language: Scala
filename: test___init__.py
change_type: ModificationType.MODIFY
diff: @@ -486,8 +486,8 @@ class TestProject:
         from openapi_python_client import GeneratorData, Project
 
         openapi = mocker.MagicMock(autospec=GeneratorData, title="My Test API")
-        tag_1 = mocker.MagicMock(autospec=str)
-        tag_2 = mocker.MagicMock(autospec=str)
+        tag_1 = "a_tag"
+        tag_2 = "another_tag"
         collection_1 = mocker.MagicMock()
         collection_2 = mocker.MagicMock()
         openapi.endpoint_collections_by_tag = {tag_1: collection_1, tag_2: collection_2}

programming_language: Python
filename: conftest.py
change_type: ModificationType.MODIFY
diff: @@ -6,7 +6,7 @@ from jinja2 import Environment, PackageLoader
 def env() -> Environment:
     from openapi_python_client import utils
 
-    TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "spinalcase": utils.spinal_case}
+    TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "kebabcase": utils.kebab_case}
     env = Environment(loader=PackageLoader("openapi_python_client"), trim_blocks=True, lstrip_blocks=True)
     env.filters.update(TEMPLATE_FILTERS)
     return env

programming_language: Python
filename: test_utils.py
change_type: ModificationType.MODIFY
diff: @@ -20,5 +20,5 @@ def test_snake_case_from_camel():
     assert utils.snake_case("httpResponseLowerCamel") == "http_response_lower_camel"
 
 
-def test_spinal_case():
-    assert utils.spinal_case("keep_alive") == "keep-alive"
+def test_kebab_case():
+    assert utils.kebab_case("keep_alive") == "keep-alive"

programming_language: Python