Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sandbox URL Creation #10

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

pixeebot[bot]
Copy link

@pixeebot pixeebot bot commented Apr 4, 2025

This codemod sandboxes calls to requests.get to be more resistant to Server-Side Request Forgery (SSRF) attacks.

Most of the time when you make a GET request to a URL, you're intending to reference an HTTP endpoint, like an internal microservice. However, URLs can point to local file system files, a Gopher stream in your local network, a JAR file on a remote Internet site, and all kinds of other unexpected and undesirable outcomes. When the URL values are influenced by attackers, they can trick your application into fetching internal resources, running malicious code, or otherwise harming the system.
Consider the following code for a Flask app:

from flask import Flask, request
import requests

app = Flask(__name__)

@app.route("/request-url")
def request_url():
    url = request.args["loc"]
    resp = requests.get(url)
    ...

In this case, an attacker could supply a value like "http://169.254.169.254/user-data/" and attempt to access user information.

Our changes introduce sandboxing around URL creation that force developers to specify some boundaries on the types of URLs they expect to create:

  from flask import Flask, request
- import requests
+ from security import safe_requests

  app = Flask(__name__)

  @app.route("/request-url")
  def request_url():
    url = request.args["loc"]
-   resp = requests.get(url)
+   resp = safe_requests.get(url)
    ...

This change alone reduces attack surface significantly because the default behavior of safe_requests.get raises a SecurityException if
a user attempts to access a known infrastructure location, unless specifically disabled.

If you have feedback on this codemod, please let us know!

F.A.Q.

Why does this codemod require a Pixee dependency?

We always prefer to use built-in Python functions or one from a well-known and trusted community dependency. However, we cannot find any such control. If you know of one, please let us know.

Why is this codemod marked as Merge After Cursory Review?

By default, the protection only weaves in 2 checks, which we believe will not cause any issues with the vast majority of code:

  1. The given URL must be HTTP/HTTPS.
  2. The given URL must not point to a "well-known infrastructure target", which includes things like AWS Metadata Service endpoints, and internal routers (e.g., 192.168.1.1) which are common targets of attacks.

However, on rare occasions an application may use a URL protocol like "file://" or "ftp://" in backend or middleware code.

If you want to allow those protocols, change the incoming PR to look more like this and get the best security possible:

-resp = requests.get(url)
+resp = safe_requests.get(url, allowed_protocols=("ftp",))

Dependency Updates

This codemod relies on an external dependency. We have automatically added this dependency to your project's requirements.txt file.

This library holds security tools for protecting Python API calls.

There are a number of places where Python project dependencies can be expressed, including setup.py, pyproject.toml, setup.cfg, and requirements.txt files. If this change is incorrect, or if you are using another packaging system such as poetry, it may be necessary for you to manually add the dependency to the proper location in your project.

More reading

🧚🤖 Powered by Pixeebot

Feedback | Community | Docs | Codemod ID: pixee:python/url-sandbox

@@ -7,3 +7,4 @@ voltage=<0.1.5a8
re
surrealdb
aiohttp
security==1.3.1
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library holds security tools for protecting Python API calls.

License: MITOpen SourceMore facts

Copy link

sonarqubecloud bot commented Apr 4, 2025

@api.get("/assets/{asset}", description="Get an asset.")
def get_asset(asset: str, width: int = None, height: int = None):
if not width and not height:
try:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that the constructed file path is contained within a safe root folder. We can achieve this by normalizing the path using os.path.normpath and then checking that the normalized path starts with the root folder. This will prevent path traversal attacks.

  1. Normalize the constructed file path using os.path.normpath.
  2. Check that the normalized path starts with the intended base path.
  3. If the check fails, return a 404 response.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -89,5 +89,10 @@
 def get_asset(asset: str, width: int = None, height: int = None):
+    base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
+    asset_path = base_path / asset
+    normalized_path = asset_path.resolve()
+    if not str(normalized_path).startswith(str(base_path)):
+        return fastapi.responses.JSONResponse(status_code=404, content={"message": "This asset does not exist."})
     if not width and not height:
         try:
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
+            return fastapi.responses.FileResponse(normalized_path)
         except:
@@ -96,16 +101,16 @@
         if asset == "logo_no_bg":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
+            image = Image.open(base_path / "Astroid Logo no bg.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
         elif asset == "logo":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
+            image = Image.open(base_path / "Astroid Logo.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid Logo{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo{width}x{height}.png")
         elif asset == "banner":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
+            image = Image.open(base_path / "resized/Astroid-banner.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid-banner{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid-banner{width}x{height}.png")
         else:
EOF
@@ -89,5 +89,10 @@
def get_asset(asset: str, width: int = None, height: int = None):
base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
asset_path = base_path / asset
normalized_path = asset_path.resolve()
if not str(normalized_path).startswith(str(base_path)):
return fastapi.responses.JSONResponse(status_code=404, content={"message": "This asset does not exist."})
if not width and not height:
try:
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
return fastapi.responses.FileResponse(normalized_path)
except:
@@ -96,16 +101,16 @@
if asset == "logo_no_bg":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
image = Image.open(base_path / "Astroid Logo no bg.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
elif asset == "logo":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
image = Image.open(base_path / "Astroid Logo.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid Logo{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo{width}x{height}.png")
elif asset == "banner":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
image = Image.open(base_path / "resized/Astroid-banner.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid-banner{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid-banner{width}x{height}.png")
else:
Copilot is powered by AI and may make mistakes. Always verify output.
if asset == "logo_no_bg":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that the constructed file paths are safe and do not allow path traversal attacks. We can achieve this by normalizing the paths and ensuring they remain within a designated safe directory. Additionally, we should validate the width and height parameters to ensure they are within acceptable ranges.

  1. Normalize the constructed file paths using os.path.normpath.
  2. Ensure the normalized paths start with the designated safe directory.
  3. Validate the width and height parameters to ensure they are positive integers.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -89,5 +89,9 @@
 def get_asset(asset: str, width: int = None, height: int = None):
+    base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
     if not width and not height:
         try:
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
+            fullpath = os.path.normpath(base_path / asset)
+            if not str(fullpath).startswith(str(base_path)):
+                raise Exception("Invalid path")
+            return fastapi.responses.FileResponse(fullpath)
         except:
@@ -95,17 +99,19 @@
     else:
+        if width <= 0 or height <= 0:
+            return fastapi.responses.JSONResponse(status_code=400, content={"message": "Invalid dimensions."})
         if asset == "logo_no_bg":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
+            image = Image.open(base_path / "Astroid Logo no bg.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
         elif asset == "logo":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
+            image = Image.open(base_path / "Astroid Logo.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid Logo{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo{width}x{height}.png")
         elif asset == "banner":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
+            image = Image.open(base_path / "resized/Astroid-banner.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid-banner{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid-banner{width}x{height}.png")
         else:
EOF
@@ -89,5 +89,9 @@
def get_asset(asset: str, width: int = None, height: int = None):
base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
if not width and not height:
try:
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
fullpath = os.path.normpath(base_path / asset)
if not str(fullpath).startswith(str(base_path)):
raise Exception("Invalid path")
return fastapi.responses.FileResponse(fullpath)
except:
@@ -95,17 +99,19 @@
else:
if width <= 0 or height <= 0:
return fastapi.responses.JSONResponse(status_code=400, content={"message": "Invalid dimensions."})
if asset == "logo_no_bg":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
image = Image.open(base_path / "Astroid Logo no bg.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
elif asset == "logo":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
image = Image.open(base_path / "Astroid Logo.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid Logo{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo{width}x{height}.png")
elif asset == "banner":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
image = Image.open(base_path / "resized/Astroid-banner.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid-banner{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid-banner{width}x{height}.png")
else:
Copilot is powered by AI and may make mistakes. Always verify output.
elif asset == "logo":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that the constructed file paths are safe and do not allow path traversal attacks. We can achieve this by normalizing the paths and ensuring they remain within a designated safe directory. Additionally, we should validate the width and height parameters to ensure they are within acceptable ranges.

  1. Normalize the constructed file paths using os.path.normpath.
  2. Ensure the normalized paths start with the designated safe directory.
  3. Validate the width and height parameters to ensure they are positive integers.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -89,5 +89,9 @@
 def get_asset(asset: str, width: int = None, height: int = None):
+    base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
     if not width and not height:
         try:
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
+            fullpath = os.path.normpath(base_path / asset)
+            if not str(fullpath).startswith(str(base_path)):
+                raise Exception("Invalid path")
+            return fastapi.responses.FileResponse(fullpath)
         except:
@@ -95,17 +99,19 @@
     else:
+        if width <= 0 or height <= 0:
+            return fastapi.responses.JSONResponse(status_code=400, content={"message": "Invalid dimensions."})
         if asset == "logo_no_bg":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
+            image = Image.open(base_path / "Astroid Logo no bg.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
         elif asset == "logo":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
+            image = Image.open(base_path / "Astroid Logo.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid Logo{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo{width}x{height}.png")
         elif asset == "banner":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
+            image = Image.open(base_path / "resized/Astroid-banner.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
+            new_image.save(base_path / f"resized/Astroid-banner{width}x{height}.png")
+            return fastapi.responses.FileResponse(base_path / f"resized/Astroid-banner{width}x{height}.png")
         else:
EOF
@@ -89,5 +89,9 @@
def get_asset(asset: str, width: int = None, height: int = None):
base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
if not width and not height:
try:
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
fullpath = os.path.normpath(base_path / asset)
if not str(fullpath).startswith(str(base_path)):
raise Exception("Invalid path")
return fastapi.responses.FileResponse(fullpath)
except:
@@ -95,17 +99,19 @@
else:
if width <= 0 or height <= 0:
return fastapi.responses.JSONResponse(status_code=400, content={"message": "Invalid dimensions."})
if asset == "logo_no_bg":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
image = Image.open(base_path / "Astroid Logo no bg.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo no bg{width}x{height}.png")
elif asset == "logo":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
image = Image.open(base_path / "Astroid Logo.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid Logo{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid Logo{width}x{height}.png")
elif asset == "banner":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
image = Image.open(base_path / "resized/Astroid-banner.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
new_image.save(base_path / f"resized/Astroid-banner{width}x{height}.png")
return fastapi.responses.FileResponse(base_path / f"resized/Astroid-banner{width}x{height}.png")
else:
Copilot is powered by AI and may make mistakes. Always verify output.
elif asset == "banner":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that the constructed file paths are safe and do not allow access to unintended files. We can achieve this by normalizing the path and ensuring it is contained within a safe root directory. Additionally, we should validate the width and height parameters to ensure they are within acceptable ranges.

  1. Normalize the constructed file path using os.path.normpath.
  2. Ensure the normalized path starts with the intended base path.
  3. Validate the width and height parameters to ensure they are positive integers.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -89,5 +89,10 @@
 def get_asset(asset: str, width: int = None, height: int = None):
+    base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
+    asset_path = base_path / asset
+    asset_path = asset_path.resolve()
+    if not str(asset_path).startswith(str(base_path)):
+        return fastapi.responses.JSONResponse(status_code=404, content={"message": "This asset does not exist."})
     if not width and not height:
         try:
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
+            return fastapi.responses.FileResponse(asset_path)
         except:
@@ -95,17 +100,22 @@
     else:
+        if width <= 0 or height <= 0:
+            return fastapi.responses.JSONResponse(status_code=400, content={"message": "Invalid dimensions."})
         if asset == "logo_no_bg":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
+            image = Image.open(base_path / "Astroid Logo no bg.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
+            resized_path = base_path / f"resized/Astroid Logo no bg{width}x{height}.png"
+            new_image.save(resized_path)
+            return fastapi.responses.FileResponse(resized_path)
         elif asset == "logo":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
+            image = Image.open(base_path / "Astroid Logo.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
+            resized_path = base_path / f"resized/Astroid Logo{width}x{height}.png"
+            new_image.save(resized_path)
+            return fastapi.responses.FileResponse(resized_path)
         elif asset == "banner":
-            image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
+            image = Image.open(base_path / "resized/Astroid-banner.png")
             new_image = image.resize((width, height))
-            new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
+            resized_path = base_path / f"resized/Astroid-banner{width}x{height}.png"
+            new_image.save(resized_path)
+            return fastapi.responses.FileResponse(resized_path)
         else:
EOF
@@ -89,5 +89,10 @@
def get_asset(asset: str, width: int = None, height: int = None):
base_path = pathlib.Path(__file__).parent.parent.resolve() / "assets"
asset_path = base_path / asset
asset_path = asset_path.resolve()
if not str(asset_path).startswith(str(base_path)):
return fastapi.responses.JSONResponse(status_code=404, content={"message": "This asset does not exist."})
if not width and not height:
try:
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/{asset}")
return fastapi.responses.FileResponse(asset_path)
except:
@@ -95,17 +100,22 @@
else:
if width <= 0 or height <= 0:
return fastapi.responses.JSONResponse(status_code=400, content={"message": "Invalid dimensions."})
if asset == "logo_no_bg":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo no bg.png")
image = Image.open(base_path / "Astroid Logo no bg.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo no bg{width}x{height}.png")
resized_path = base_path / f"resized/Astroid Logo no bg{width}x{height}.png"
new_image.save(resized_path)
return fastapi.responses.FileResponse(resized_path)
elif asset == "logo":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/Astroid Logo.png")
image = Image.open(base_path / "Astroid Logo.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid Logo{width}x{height}.png")
resized_path = base_path / f"resized/Astroid Logo{width}x{height}.png"
new_image.save(resized_path)
return fastapi.responses.FileResponse(resized_path)
elif asset == "banner":
image = Image.open(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
image = Image.open(base_path / "resized/Astroid-banner.png")
new_image = image.resize((width, height))
new_image.save(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner.png")
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.parent.resolve()}/assets/resized/Astroid-banner{width}x{height}.png")
resized_path = base_path / f"resized/Astroid-banner{width}x{height}.png"
new_image.save(resized_path)
return fastapi.responses.FileResponse(resized_path)
else:
Copilot is powered by AI and may make mistakes. Always verify output.
async def get_cdn_asset(assetId: str):
asset = await astroidapi.surrealdb_handler.AttachmentProcessor.get_attachment(assetId)
try:
if asset:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that the constructed file path is safe and does not allow path traversal attacks. We can achieve this by normalizing the path and ensuring it remains within the intended directory. Specifically, we will:

  1. Normalize the constructed file path using os.path.normpath.
  2. Check that the normalized path starts with the intended base directory.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -165,3 +165,7 @@
         if asset:
-            return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.resolve()}/astroidapi/TMP_attachments/{assetId}.{asset['type']}")
+            base_path = pathlib.Path(__file__).parent.resolve() / "astroidapi/TMP_attachments"
+            full_path = os.path.normpath(base_path / f"{assetId}.{asset['type']}")
+            if not str(full_path).startswith(str(base_path)):
+                raise HTTPException(status_code=400, detail="Invalid asset path.")
+            return fastapi.responses.FileResponse(full_path)
         else:
EOF
@@ -165,3 +165,7 @@
if asset:
return fastapi.responses.FileResponse(f"{pathlib.Path(__file__).parent.resolve()}/astroidapi/TMP_attachments/{assetId}.{asset['type']}")
base_path = pathlib.Path(__file__).parent.resolve() / "astroidapi/TMP_attachments"
full_path = os.path.normpath(base_path / f"{assetId}.{asset['type']}")
if not str(full_path).startswith(str(base_path)):
raise HTTPException(status_code=400, detail="Invalid asset path.")
return fastapi.responses.FileResponse(full_path)
else:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +486 to +487
except astroidapi.errors.HealtCheckError.EndpointCheckError as e:
return fastapi.responses.JSONResponse(status_code=200, content={"message": f"An error occurred: {e}",

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that detailed error messages and stack traces are not exposed to the user. Instead, we should log the detailed error information on the server and return a generic error message to the user. This can be achieved by modifying the exception handling code to log the exception and return a generic error message.

  1. Modify the exception handling code to log the detailed error message using logging.exception().
  2. Return a generic error message to the user without including the exception details.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -486,3 +486,4 @@
         except astroidapi.errors.HealtCheckError.EndpointCheckError as e:
-            return fastapi.responses.JSONResponse(status_code=200, content={"message": f"An error occurred: {e}",
+            logging.exception("An error occurred during endpoint health check.")
+            return fastapi.responses.JSONResponse(status_code=200, content={"message": "An unexpected error occurred.",
                                                                             "details": "unexpectederror"})
@@ -492,4 +493,4 @@
         except astroidapi.errors.SurrealDBHandler.GetEndpointError as e:
-            traceback.print_exc()
-            return fastapi.responses.JSONResponse(status_code=404, content={"message": f"An error occurred: {e}",
+            logging.exception("An error occurred while getting the endpoint.")
+            return fastapi.responses.JSONResponse(status_code=404, content={"message": "An error occurred while getting the endpoint.",
                                                                             "details": "getendpointerror"})
EOF
@@ -486,3 +486,4 @@
except astroidapi.errors.HealtCheckError.EndpointCheckError as e:
return fastapi.responses.JSONResponse(status_code=200, content={"message": f"An error occurred: {e}",
logging.exception("An error occurred during endpoint health check.")
return fastapi.responses.JSONResponse(status_code=200, content={"message": "An unexpected error occurred.",
"details": "unexpectederror"})
@@ -492,4 +493,4 @@
except astroidapi.errors.SurrealDBHandler.GetEndpointError as e:
traceback.print_exc()
return fastapi.responses.JSONResponse(status_code=404, content={"message": f"An error occurred: {e}",
logging.exception("An error occurred while getting the endpoint.")
return fastapi.responses.JSONResponse(status_code=404, content={"message": "An error occurred while getting the endpoint.",
"details": "getendpointerror"})
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +493 to +494
traceback.print_exc()
return fastapi.responses.JSONResponse(status_code=404, content={"message": f"An error occurred: {e}",

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that detailed error information is not exposed to the user. Instead, we should log the detailed error information on the server and return a generic error message to the user. This can be achieved by modifying the exception handling code to log the exception details and return a generic error message.

  1. Modify the exception handling code to log the detailed error information using logging.exception().
  2. Return a generic error message to the user without including the exception details.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -460,4 +460,4 @@
         except Exception as e:
-            logging.exception(traceback.print_exc())
-            return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
+            logging.exception("An error occurred while marking read.")
+            return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})
         return fastapi.responses.JSONResponse(status_code=200, content={"message": "Success."})
@@ -492,4 +492,4 @@
         except astroidapi.errors.SurrealDBHandler.GetEndpointError as e:
-            traceback.print_exc()
-            return fastapi.responses.JSONResponse(status_code=404, content={"message": f"An error occurred: {e}",
+            logging.exception("An error occurred while getting endpoint information.")
+            return fastapi.responses.JSONResponse(status_code=404, content={"message": "An internal error has occurred.",
                                                                             "details": "getendpointerror"})
@@ -508,4 +508,4 @@
         except Exception as e:
-            logging.exception(traceback.print_exc())
-            return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
+            logging.exception("An error occurred while repairing the endpoint.")
+            return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})
     else:
EOF
@@ -460,4 +460,4 @@
except Exception as e:
logging.exception(traceback.print_exc())
return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
logging.exception("An error occurred while marking read.")
return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})
return fastapi.responses.JSONResponse(status_code=200, content={"message": "Success."})
@@ -492,4 +492,4 @@
except astroidapi.errors.SurrealDBHandler.GetEndpointError as e:
traceback.print_exc()
return fastapi.responses.JSONResponse(status_code=404, content={"message": f"An error occurred: {e}",
logging.exception("An error occurred while getting endpoint information.")
return fastapi.responses.JSONResponse(status_code=404, content={"message": "An internal error has occurred.",
"details": "getendpointerror"})
@@ -508,4 +508,4 @@
except Exception as e:
logging.exception(traceback.print_exc())
return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
logging.exception("An error occurred while repairing the endpoint.")
return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})
else:
Copilot is powered by AI and may make mistakes. Always verify output.
summary = await astroidapi.health_check.HealthCheck.EndpointCheck.repair_structure(endpoint)
return fastapi.responses.JSONResponse(status_code=200, content={"message": "Repaired.", "summary": summary})
except Exception as e:
logging.exception(traceback.print_exc())

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 10 days ago

To fix the problem, we should avoid including the exception message in the response sent to the user. Instead, we can log the detailed error message on the server and return a generic error message to the user. This approach ensures that sensitive information is not exposed while still allowing developers to debug the issue using the server logs.

Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -508,4 +508,4 @@
         except Exception as e:
-            logging.exception(traceback.print_exc())
-            return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
+            logging.exception(traceback.format_exc())
+            return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})
     else:
EOF
@@ -508,4 +508,4 @@
except Exception as e:
logging.exception(traceback.print_exc())
return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
logging.exception(traceback.format_exc())
return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})
else:
Copilot is powered by AI and may make mistakes. Always verify output.
return fastapi.responses.JSONResponse(status_code=401, content={"message": "You must provide a token."})
except KeyError:
if token == Bot.config.MASTER_TOKEN:
try:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that the constructed file path is contained within a safe root folder. We can achieve this by normalizing the path using os.path.normpath and then checking that the normalized path starts with the root folder. This will prevent directory traversal attacks by ensuring that the endpoint value cannot be used to access files outside the intended directory.

  1. Normalize the constructed file path using os.path.normpath.
  2. Check that the normalized path starts with the root folder.
  3. Raise an exception if the path is not within the root folder.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -597,3 +597,8 @@
             try:
-                os.remove(f"{pathlib.Path(__file__).parent.resolve()}/endpoints/{endpoint}.json")
+                base_path = pathlib.Path(__file__).parent.resolve() / "endpoints"
+                file_path = base_path / f"{endpoint}.json"
+                normalized_path = os.path.normpath(file_path)
+                if not str(normalized_path).startswith(str(base_path)):
+                    raise HTTPException(status_code=400, detail="Invalid endpoint path.")
+                os.remove(normalized_path)
                 return fastapi.responses.JSONResponse(status_code=200, content={"message": "Deleted."})
EOF
@@ -597,3 +597,8 @@
try:
os.remove(f"{pathlib.Path(__file__).parent.resolve()}/endpoints/{endpoint}.json")
base_path = pathlib.Path(__file__).parent.resolve() / "endpoints"
file_path = base_path / f"{endpoint}.json"
normalized_path = os.path.normpath(file_path)
if not str(normalized_path).startswith(str(base_path)):
raise HTTPException(status_code=400, detail="Invalid endpoint path.")
os.remove(normalized_path)
return fastapi.responses.JSONResponse(status_code=200, content={"message": "Deleted."})
Copilot is powered by AI and may make mistakes. Always verify output.
try:
await astroidapi.surrealdb_handler.sync_server_relations()
return fastapi.responses.JSONResponse(status_code=200, content={"message": "Success."})
except Exception as e:

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 10 days ago

To fix the problem, we need to ensure that detailed error messages, including stack traces, are not exposed to end users. Instead, we should log the detailed error information on the server and return a generic error message to the user. This can be achieved by modifying the exception handling code to log the exception and return a generic message.

  1. Import the logging module if not already imported.
  2. Modify the exception handling block to log the detailed error message using logging.error.
  3. Return a generic error message to the user.
Suggested changeset 1
src/api.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -757,3 +757,4 @@
     except Exception as e:
-        return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
+        logging.error(f"An error occurred: {e}")
+        return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})
 
EOF
@@ -757,3 +757,4 @@
except Exception as e:
return fastapi.responses.JSONResponse(status_code=500, content={"message": f"An error occurred: {e}"})
logging.error(f"An error occurred: {e}")
return fastapi.responses.JSONResponse(status_code=500, content={"message": "An internal error has occurred."})

Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Author

pixeebot bot commented Apr 12, 2025

I'm confident in this change, but I'm not a maintainer of this project. Do you see any reason not to merge it?

If this change was not helpful, or you have suggestions for improvements, please let me know!

Copy link
Author

pixeebot bot commented Apr 13, 2025

Just a friendly ping to remind you about this change. If there are concerns about it, we'd love to hear about them!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants