Skip to content

Conversation

@trevor-e
Copy link
Member

No description provided.

# Check if artifact exists
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"

if not artifact_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

To fix this issue, the code should ensure that the file being accessed is strictly within the ARTIFACTS_DIR directory. The best-practice method is to:

  1. Join the user-provided artifact_id (plus extension) with the base directory;
  2. Normalize the resulting path using os.path.normpath or (for Path) .resolve();
  3. Verify that the resulting path is still within (a child of) the base directory;
  4. Only proceed to access the file if these checks pass.

In this snippet, in download_artifact, the lines constructing and checking artifact_path starting at line 71 must be changed. The check should:

  • Use Path.resolve() to get the normalized absolute path for the requested file,
  • Make sure the resolved path is within ARTIFACTS_DIR (using .relative_to() or path prefix checking).
    If the check fails, raise a 400 or 403 error.

No new custom helper methods are strictly required, but the code will need a proper check sequence as described above. The import for os already exists, and Path is already imported, so no new imports are required.


Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -68,7 +68,13 @@
 ):
     """Download artifact file."""
     # Check if artifact exists
-    artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
+    # Safely join and normalize artifact path
+    artifact_path = (ARTIFACTS_DIR / f"{artifact_id}.zip").resolve()
+    # Prevent path traversal outside the artifacts directory
+    try:
+        artifact_path.relative_to(ARTIFACTS_DIR.resolve())
+    except ValueError:
+        raise HTTPException(status_code=400, detail="Invalid artifact_id path")
 
     if not artifact_path.exists():
         raise HTTPException(status_code=404, detail="Artifact not found")
EOF
@@ -68,7 +68,13 @@
):
"""Download artifact file."""
# Check if artifact exists
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
# Safely join and normalize artifact path
artifact_path = (ARTIFACTS_DIR / f"{artifact_id}.zip").resolve()
# Prevent path traversal outside the artifacts directory
try:
artifact_path.relative_to(ARTIFACTS_DIR.resolve())
except ValueError:
raise HTTPException(status_code=400, detail="Invalid artifact_id path")

if not artifact_path.exists():
raise HTTPException(status_code=404, detail="Artifact not found")
Copilot is powered by AI and may make mistakes. Always verify output.

# Handle HEAD request
if request.method == "HEAD":
file_size = artifact_path.stat().st_size

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

To properly fix this vulnerability, we must ensure that the resulting artifact_path—after joining the user input—remains within the intended artifacts root directory (ARTIFACTS_DIR). This is best done by:

  • Composing the final path (with any extensions/postfixes) as before.
  • Normalizing the resulting path with .resolve() (or os.path.normpath/os.path.realpath) to remove any traversal elements.
  • Verifying that the normalized path is a descendant (or the same as) the allowed directory (ARTIFACTS_DIR). This can be checked by using Path.is_relative_to (Python 3.9+) or by comparing path components if using an older version.
  • If the check fails, raise a 400 or 403 error.

All changes must be made only in the shown snippet, affecting the download_artifact function (lines 62–95 or so), specifically the calculation and security checking of artifact_path.


Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -68,7 +68,15 @@
 ):
     """Download artifact file."""
     # Check if artifact exists
-    artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
+    artifact_path = (ARTIFACTS_DIR / f"{artifact_id}.zip").resolve()
+    # Ensure resolved artifact_path is within ARTIFACTS_DIR to prevent path traversal
+    try:
+        if not artifact_path.is_relative_to(ARTIFACTS_DIR.resolve()):
+            raise HTTPException(status_code=400, detail="Invalid artifact id")
+    except AttributeError:
+        # For Python <3.9 fallback
+        if not str(artifact_path).startswith(str(ARTIFACTS_DIR.resolve()) + os.sep):
+            raise HTTPException(status_code=400, detail="Invalid artifact id")
 
     if not artifact_path.exists():
         raise HTTPException(status_code=404, detail="Artifact not found")
EOF
@@ -68,7 +68,15 @@
):
"""Download artifact file."""
# Check if artifact exists
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
artifact_path = (ARTIFACTS_DIR / f"{artifact_id}.zip").resolve()
# Ensure resolved artifact_path is within ARTIFACTS_DIR to prevent path traversal
try:
if not artifact_path.is_relative_to(ARTIFACTS_DIR.resolve()):
raise HTTPException(status_code=400, detail="Invalid artifact id")
except AttributeError:
# For Python <3.9 fallback
if not str(artifact_path).startswith(str(ARTIFACTS_DIR.resolve()) + os.sep):
raise HTTPException(status_code=400, detail="Invalid artifact id")

if not artifact_path.exists():
raise HTTPException(status_code=404, detail="Artifact not found")
Copilot is powered by AI and may make mistakes. Always verify output.
if range_header:
# Parse range header (simplified implementation)
range_start = int(range_header.replace("bytes=", "").split("-")[0])
with open(artifact_path, "rb") as f:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

The best general fix is to ensure the constructed artifact_path cannot be manipulated to escape the intended ARTIFACTS_DIR. This is well-achieved by normalizing the resulting path (with .resolve()) and then checking that the resolved path is indeed inside the allowed directory. Specifically:

  • After constructing artifact_path, call .resolve() on it.
  • Check that the resolved path is a subpath (or the same path) as ARTIFACTS_DIR.resolve() (e.g., using is_relative_to in Python 3.9+, or a string startswith in older versions).
  • Only proceed to open the file if this check passes; otherwise, return a 403 error.
  • These changes should be applied to the download endpoint in server.py, lines 71–75 (right after constructing artifact_path).

Requirements:

  • From Python 3.9+, Path.is_relative_to can be used (we'll assume Python 3.9+ since FastAPI is reasonably modern).
  • No extra dependencies needed; use the standard library.
Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -69,13 +69,20 @@
     """Download artifact file."""
     # Check if artifact exists
     artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
+    # Prevent path traversal by resolving and ensuring within ARTIFACTS_DIR
+    try:
+        resolved_artifact_path = artifact_path.resolve(strict=False)
+        if not resolved_artifact_path.is_relative_to(ARTIFACTS_DIR.resolve()):
+            raise HTTPException(status_code=403, detail="Access denied")
+    except Exception:
+        raise HTTPException(status_code=400, detail="Invalid artifact path")
 
-    if not artifact_path.exists():
+    if not resolved_artifact_path.exists():
         raise HTTPException(status_code=404, detail="Artifact not found")
 
     # Handle HEAD request
     if request.method == "HEAD":
-        file_size = artifact_path.stat().st_size
+        file_size = resolved_artifact_path.stat().st_size
         return Response(headers={"Content-Length": str(file_size)}, status_code=200)
 
     # Handle Range requests for resumable downloads
@@ -83,7 +85,7 @@
     if range_header:
         # Parse range header (simplified implementation)
         range_start = int(range_header.replace("bytes=", "").split("-")[0])
-        with open(artifact_path, "rb") as f:
+        with open(resolved_artifact_path, "rb") as f:
             f.seek(range_start)
             content = f.read()
         return Response(
@@ -92,7 +94,7 @@
             headers={"Content-Range": f"bytes {range_start}-{len(content) - 1}/{artifact_path.stat().st_size}"},
         )
 
-    return FileResponse(artifact_path)
+    return FileResponse(resolved_artifact_path)
 
 
 class UpdateRequest(BaseModel):
EOF
@@ -69,13 +69,20 @@
"""Download artifact file."""
# Check if artifact exists
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
# Prevent path traversal by resolving and ensuring within ARTIFACTS_DIR
try:
resolved_artifact_path = artifact_path.resolve(strict=False)
if not resolved_artifact_path.is_relative_to(ARTIFACTS_DIR.resolve()):
raise HTTPException(status_code=403, detail="Access denied")
except Exception:
raise HTTPException(status_code=400, detail="Invalid artifact path")

if not artifact_path.exists():
if not resolved_artifact_path.exists():
raise HTTPException(status_code=404, detail="Artifact not found")

# Handle HEAD request
if request.method == "HEAD":
file_size = artifact_path.stat().st_size
file_size = resolved_artifact_path.stat().st_size
return Response(headers={"Content-Length": str(file_size)}, status_code=200)

# Handle Range requests for resumable downloads
@@ -83,7 +85,7 @@
if range_header:
# Parse range header (simplified implementation)
range_start = int(range_header.replace("bytes=", "").split("-")[0])
with open(artifact_path, "rb") as f:
with open(resolved_artifact_path, "rb") as f:
f.seek(range_start)
content = f.read()
return Response(
@@ -92,7 +94,7 @@
headers={"Content-Range": f"bytes {range_start}-{len(content) - 1}/{artifact_path.stat().st_size}"},
)

return FileResponse(artifact_path)
return FileResponse(resolved_artifact_path)


class UpdateRequest(BaseModel):
Copilot is powered by AI and may make mistakes. Always verify output.
return Response(
content=content,
status_code=206,
headers={"Content-Range": f"bytes {range_start}-{len(content) - 1}/{artifact_path.stat().st_size}"},

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

To fix this vulnerability, the server should ensure that any file access using a filename derived from user input (here, artifact_id) does not allow traversal outside of the intended directory (ARTIFACTS_DIR). The simplest and most robust approach is to:

  1. Join and normalize the path (using os.path.normpath or Path.resolve) after combining the base directory and the constructed filename.
  2. Check that the resolved path is within the intended directory (i.e., that the resolved path starts with or is a descendant of ARTIFACTS_DIR). If not, return a 400 or 403 error.
  3. Perform existing file system operations only after this check passes.

This requires modifying the code at/near places where artifact_path is constructed and before it is used, in the download_artifact endpoint. No new methods are strictly needed—existing code can use the standard library pathlib and/or os.


Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -68,7 +68,14 @@
 ):
     """Download artifact file."""
     # Check if artifact exists
-    artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
+    # Prevent directory traversal: ensure resolved path is within ARTIFACTS_DIR
+    unsafe_artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
+    try:
+        artifact_path = unsafe_artifact_path.resolve(strict=False)
+    except Exception:
+        raise HTTPException(status_code=400, detail="Invalid artifact path")
+    if not str(artifact_path).startswith(str(ARTIFACTS_DIR.resolve())):
+        raise HTTPException(status_code=403, detail="Access to artifact denied")
 
     if not artifact_path.exists():
         raise HTTPException(status_code=404, detail="Artifact not found")
EOF
@@ -68,7 +68,14 @@
):
"""Download artifact file."""
# Check if artifact exists
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
# Prevent directory traversal: ensure resolved path is within ARTIFACTS_DIR
unsafe_artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
try:
artifact_path = unsafe_artifact_path.resolve(strict=False)
except Exception:
raise HTTPException(status_code=400, detail="Invalid artifact path")
if not str(artifact_path).startswith(str(ARTIFACTS_DIR.resolve())):
raise HTTPException(status_code=403, detail="Access to artifact denied")

if not artifact_path.exists():
raise HTTPException(status_code=404, detail="Artifact not found")
Copilot is powered by AI and may make mistakes. Always verify output.
headers={"Content-Range": f"bytes {range_start}-{len(content) - 1}/{artifact_path.stat().st_size}"},
)

return FileResponse(artifact_path)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

General fix:
To prevent directory traversal or unintended file access, before accessing any file path that includes user input (like artifact_id), you must validate and constrain the resolved path to ensure it does not escape a designated root directory.

Best fix for this scenario:
Normalize and join the path using os.path.normpath (or PurePath if using pathlib), and then verify that the resulting path is within ARTIFACTS_DIR. If it is outside, abort with an error. You may also wish to further restrict artifact_id (e.g., only allow alphanumerics and -/_) using a regex, but the normalization+containment check is most important.

Implementation in code:

  • After composing artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip", resolve artifact_path to its absolute/canonical form (using .resolve()).
  • Check that the resolved path is a child of (or the same as) ARTIFACTS_DIR.resolve().
  • If not, raise a 400 error or similar.

Required imports/methods:

  • No new external dependencies are needed, Pathlib already supports .resolve() and path inclusion checks.

Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -69,8 +69,13 @@
     """Download artifact file."""
     # Check if artifact exists
     artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
+    # Normalize and validate the path to prevent directory traversal
+    resolved_artifact_path = artifact_path.resolve()
+    artifacts_dir_resolved = ARTIFACTS_DIR.resolve()
+    if not str(resolved_artifact_path).startswith(str(artifacts_dir_resolved) + os.sep):
+        raise HTTPException(status_code=400, detail="Invalid artifact_id path")
 
-    if not artifact_path.exists():
+    if not resolved_artifact_path.exists():
         raise HTTPException(status_code=404, detail="Artifact not found")
 
     # Handle HEAD request
EOF
@@ -69,8 +69,13 @@
"""Download artifact file."""
# Check if artifact exists
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
# Normalize and validate the path to prevent directory traversal
resolved_artifact_path = artifact_path.resolve()
artifacts_dir_resolved = ARTIFACTS_DIR.resolve()
if not str(resolved_artifact_path).startswith(str(artifacts_dir_resolved) + os.sep):
raise HTTPException(status_code=400, detail="Invalid artifact_id path")

if not artifact_path.exists():
if not resolved_artifact_path.exists():
raise HTTPException(status_code=404, detail="Artifact not found")

# Handle HEAD request
Copilot is powered by AI and may make mistakes. Always verify output.
"""Test helper: Upload an artifact file for testing."""
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"

with open(artifact_path, "wb") as f:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

To fix the issue while preserving current behavior, the code should ensure user-provided artifact_id cannot escape the intended artifact storage directory (ARTIFACTS_DIR). The best general-purpose fix is to validate and sanitize the resulting file path. This can be done by joining and normalizing the path, then ensuring it resides within the intended directory via a prefix check, as recommended in the background section.

Specifically, before opening the file at artifact_path, do the following:

  • Use os.path.normpath() or Path.resolve() to get the canonical final path.
  • Ensure this normalized path is still within ARTIFACTS_DIR.
  • If the check fails, return an error (HTTP 400 or 403).

This change is needed within the test_upload_artifact function, lines 295-299. No changes to imports are required since pathlib.Path is already imported.


Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -294,7 +294,12 @@
     """Test helper: Upload an artifact file for testing."""
     artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"
 
-    with open(artifact_path, "wb") as f:
+    # Validate that the resolved path is within the ARTIFACTS_DIR
+    resolved_path = artifact_path.resolve()
+    if not str(resolved_path).startswith(str(ARTIFACTS_DIR.resolve()) + os.sep):
+        raise HTTPException(status_code=400, detail="Invalid artifact_id path")
+
+    with open(resolved_path, "wb") as f:
         content = await file.read()
         f.write(content)
 
EOF
@@ -294,7 +294,12 @@
"""Test helper: Upload an artifact file for testing."""
artifact_path = ARTIFACTS_DIR / f"{artifact_id}.zip"

with open(artifact_path, "wb") as f:
# Validate that the resolved path is within the ARTIFACTS_DIR
resolved_path = artifact_path.resolve()
if not str(resolved_path).startswith(str(ARTIFACTS_DIR.resolve()) + os.sep):
raise HTTPException(status_code=400, detail="Invalid artifact_id path")

with open(resolved_path, "wb") as f:
content = await file.read()
f.write(content)

Copilot is powered by AI and may make mistakes. Always verify output.
return {
"artifact_metadata": artifacts_db.get(artifact_id, {}),
"size_analysis": size_analysis_db.get(artifact_id, {}),
"has_size_analysis_file": (RESULTS_DIR / f"{artifact_id}_size_analysis.json").exists(),

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

The best way to fix this problem is to validate, before using, that the constructed file path is both safe and strictly contained within the intended parent directory (RESULTS_DIR). This can be accomplished by first joining and normalizing the constructed path with os.path.normpath (or using resolve() from pathlib), and then checking that the resulting path is still within RESULTS_DIR. If not, return an error. This solution avoids directory traversal as well as absolute-path targeting.

Concretely, in test_get_results, before any use of artifact_id to assemble a file path (in both lines 310 and 311), normalize the path and check containment:

  • Build the full path using RESULTS_DIR / f"{artifact_id}_size_analysis.json" (and similarly for artifact_id_app).
  • Call .resolve() on the result.
  • Check if the resolved path is a child (descendant) of RESULTS_DIR.resolve(). If not, return an HTTP 400 or 403 error or set the flag as False (as appropriate).

Add an import for os or use Pathlib's facilities as the rest of the file does.


Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -304,11 +304,26 @@
 @app.get("/test/results/{artifact_id}")
 async def test_get_results(artifact_id: str):
     """Test helper: Get analysis results for an artifact."""
+    # Validate artifact_id to prevent path traversal
+    size_analysis_path = (RESULTS_DIR / f"{artifact_id}_size_analysis.json")
+    app_path = (RESULTS_DIR / f"{artifact_id}_app")
+
+    try:
+        # Ensure both are within RESULTS_DIR
+        size_analysis_path_resolved = size_analysis_path.resolve()
+        app_path_resolved = app_path.resolve()
+        results_dir_resolved = RESULTS_DIR.resolve()
+        has_size_analysis_file = size_analysis_path_resolved.parent == results_dir_resolved and size_analysis_path_resolved.exists()
+        has_installable_app = app_path_resolved.parent == results_dir_resolved and app_path_resolved.exists()
+    except Exception:
+        has_size_analysis_file = False
+        has_installable_app = False
+
     return {
         "artifact_metadata": artifacts_db.get(artifact_id, {}),
         "size_analysis": size_analysis_db.get(artifact_id, {}),
-        "has_size_analysis_file": (RESULTS_DIR / f"{artifact_id}_size_analysis.json").exists(),
-        "has_installable_app": (RESULTS_DIR / f"{artifact_id}_app").exists(),
+        "has_size_analysis_file": has_size_analysis_file,
+        "has_installable_app": has_installable_app,
     }
 
 
EOF
@@ -304,11 +304,26 @@
@app.get("/test/results/{artifact_id}")
async def test_get_results(artifact_id: str):
"""Test helper: Get analysis results for an artifact."""
# Validate artifact_id to prevent path traversal
size_analysis_path = (RESULTS_DIR / f"{artifact_id}_size_analysis.json")
app_path = (RESULTS_DIR / f"{artifact_id}_app")

try:
# Ensure both are within RESULTS_DIR
size_analysis_path_resolved = size_analysis_path.resolve()
app_path_resolved = app_path.resolve()
results_dir_resolved = RESULTS_DIR.resolve()
has_size_analysis_file = size_analysis_path_resolved.parent == results_dir_resolved and size_analysis_path_resolved.exists()
has_installable_app = app_path_resolved.parent == results_dir_resolved and app_path_resolved.exists()
except Exception:
has_size_analysis_file = False
has_installable_app = False

return {
"artifact_metadata": artifacts_db.get(artifact_id, {}),
"size_analysis": size_analysis_db.get(artifact_id, {}),
"has_size_analysis_file": (RESULTS_DIR / f"{artifact_id}_size_analysis.json").exists(),
"has_installable_app": (RESULTS_DIR / f"{artifact_id}_app").exists(),
"has_size_analysis_file": has_size_analysis_file,
"has_installable_app": has_installable_app,
}


Copilot is powered by AI and may make mistakes. Always verify output.
"artifact_metadata": artifacts_db.get(artifact_id, {}),
"size_analysis": size_analysis_db.get(artifact_id, {}),
"has_size_analysis_file": (RESULTS_DIR / f"{artifact_id}_size_analysis.json").exists(),
"has_installable_app": (RESULTS_DIR / f"{artifact_id}_app").exists(),

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

The best way to fix this issue is to ensure that any filename or file path constructed using untrusted input (artifact_id) cannot escape the intended "root" directory (RESULTS_DIR). To do this, we'll first construct the intended path, then normalize it using os.path.normpath or equivalent. We'll then ensure that the normalized path is still strictly within (i.e., starts with) the canonical path to RESULTS_DIR. If it does not, we raise an error.

Specifically, in the test_get_results endpoint, before using (RESULTS_DIR / f"{artifact_id}_size_analysis.json") and (RESULTS_DIR / f"{artifact_id}_app"), we will resolve and validate the filenames. This can be factored into a small helper function for reusability (since the same fix pattern will be used for both). No new functionality should be introduced; just extra checking and safe path construction. As minimum required, import os if not already, and provide the validation logic only using standard libraries.


Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -15,12 +15,26 @@
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
+import os
+
 from fastapi import FastAPI, Header, HTTPException, Request, Response, UploadFile
 from fastapi.responses import FileResponse, JSONResponse
 from pydantic import BaseModel
 
 app = FastAPI(title="Mock Sentry API for Launchpad E2E Tests")
 
+def safe_artifact_file_path(artifact_id: str, root_dir: Path, suffix: str) -> Path:
+    """
+    Safely build a file path for an artifact, ensuring it stays within the root_dir.
+    """
+    unsafe_path = root_dir / f"{artifact_id}{suffix}"
+    # Resolve absolute path (without actually creating files), prevent symlink attacks.
+    normalized_path = unsafe_path.resolve()
+    root_dir_resolved = root_dir.resolve()
+    if not str(normalized_path).startswith(str(root_dir_resolved) + os.sep):
+        raise HTTPException(status_code=400, detail="Invalid artifact_id")
+    return normalized_path
+
 # Storage paths
 DATA_DIR = Path("/app/data")
 ARTIFACTS_DIR = DATA_DIR / "artifacts"
@@ -304,18 +312,20 @@
 @app.get("/test/results/{artifact_id}")
 async def test_get_results(artifact_id: str):
     """Test helper: Get analysis results for an artifact."""
+    size_analysis_file = safe_artifact_file_path(artifact_id, RESULTS_DIR, "_size_analysis.json")
+    app_file = safe_artifact_file_path(artifact_id, RESULTS_DIR, "_app")
     return {
         "artifact_metadata": artifacts_db.get(artifact_id, {}),
         "size_analysis": size_analysis_db.get(artifact_id, {}),
-        "has_size_analysis_file": (RESULTS_DIR / f"{artifact_id}_size_analysis.json").exists(),
-        "has_installable_app": (RESULTS_DIR / f"{artifact_id}_app").exists(),
+        "has_size_analysis_file": size_analysis_file.exists(),
+        "has_installable_app": app_file.exists(),
     }
 
 
 @app.get("/test/results/{artifact_id}/size-analysis-raw")
 async def test_get_size_analysis_raw(artifact_id: str):
     """Test helper: Get raw size analysis JSON."""
-    result_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"
+    result_path = safe_artifact_file_path(artifact_id, RESULTS_DIR, "_size_analysis.json")
 
     if not result_path.exists():
         raise HTTPException(status_code=404, detail="Size analysis not found")
EOF
@@ -15,12 +15,26 @@
from pathlib import Path
from typing import Any, Dict, List, Optional

import os

from fastapi import FastAPI, Header, HTTPException, Request, Response, UploadFile
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel

app = FastAPI(title="Mock Sentry API for Launchpad E2E Tests")

def safe_artifact_file_path(artifact_id: str, root_dir: Path, suffix: str) -> Path:
"""
Safely build a file path for an artifact, ensuring it stays within the root_dir.
"""
unsafe_path = root_dir / f"{artifact_id}{suffix}"
# Resolve absolute path (without actually creating files), prevent symlink attacks.
normalized_path = unsafe_path.resolve()
root_dir_resolved = root_dir.resolve()
if not str(normalized_path).startswith(str(root_dir_resolved) + os.sep):
raise HTTPException(status_code=400, detail="Invalid artifact_id")
return normalized_path

# Storage paths
DATA_DIR = Path("/app/data")
ARTIFACTS_DIR = DATA_DIR / "artifacts"
@@ -304,18 +312,20 @@
@app.get("/test/results/{artifact_id}")
async def test_get_results(artifact_id: str):
"""Test helper: Get analysis results for an artifact."""
size_analysis_file = safe_artifact_file_path(artifact_id, RESULTS_DIR, "_size_analysis.json")
app_file = safe_artifact_file_path(artifact_id, RESULTS_DIR, "_app")
return {
"artifact_metadata": artifacts_db.get(artifact_id, {}),
"size_analysis": size_analysis_db.get(artifact_id, {}),
"has_size_analysis_file": (RESULTS_DIR / f"{artifact_id}_size_analysis.json").exists(),
"has_installable_app": (RESULTS_DIR / f"{artifact_id}_app").exists(),
"has_size_analysis_file": size_analysis_file.exists(),
"has_installable_app": app_file.exists(),
}


@app.get("/test/results/{artifact_id}/size-analysis-raw")
async def test_get_size_analysis_raw(artifact_id: str):
"""Test helper: Get raw size analysis JSON."""
result_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"
result_path = safe_artifact_file_path(artifact_id, RESULTS_DIR, "_size_analysis.json")

if not result_path.exists():
raise HTTPException(status_code=404, detail="Size analysis not found")
Copilot is powered by AI and may make mistakes. Always verify output.
"""Test helper: Get raw size analysis JSON."""
result_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"

if not result_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

To fix this, the code should ensure that any file path computed using user input remains inside the intended directory. The best-practice approach is to normalize the constructed path (with os.path.normpath or similar) and ensure it is still under the intended directory (in this case, RESULTS_DIR). This can be done by resolving the path and checking that it starts with the RESULTS_DIR. If the check fails, return a 400 or 403 error. Only then should the file access proceed.

The edit is required only around the usage of artifact_id for the file path, under test_get_size_analysis_raw in tests/e2e/mock-sentry-api/server.py.

You may need to import os or use methods from pathlib, but as os and pathlib are already imported, no new imports are needed.


Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -315,7 +315,14 @@
 @app.get("/test/results/{artifact_id}/size-analysis-raw")
 async def test_get_size_analysis_raw(artifact_id: str):
     """Test helper: Get raw size analysis JSON."""
-    result_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"
+    # Validate artifact_id to prevent path traversal
+    candidate_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"
+    try:
+        result_path = candidate_path.resolve(strict=False)
+    except Exception:
+        raise HTTPException(status_code=400, detail="Invalid artifact ID")
+    if not str(result_path).startswith(str(RESULTS_DIR.resolve())):
+        raise HTTPException(status_code=400, detail="Invalid artifact ID")
 
     if not result_path.exists():
         raise HTTPException(status_code=404, detail="Size analysis not found")
EOF
@@ -315,7 +315,14 @@
@app.get("/test/results/{artifact_id}/size-analysis-raw")
async def test_get_size_analysis_raw(artifact_id: str):
"""Test helper: Get raw size analysis JSON."""
result_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"
# Validate artifact_id to prevent path traversal
candidate_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"
try:
result_path = candidate_path.resolve(strict=False)
except Exception:
raise HTTPException(status_code=400, detail="Invalid artifact ID")
if not str(result_path).startswith(str(RESULTS_DIR.resolve())):
raise HTTPException(status_code=400, detail="Invalid artifact ID")

if not result_path.exists():
raise HTTPException(status_code=404, detail="Size analysis not found")
Copilot is powered by AI and may make mistakes. Always verify output.
if not result_path.exists():
raise HTTPException(status_code=404, detail="Size analysis not found")

return JSONResponse(json.loads(result_path.read_text()))

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High test

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 days ago

To fix this path traversal vulnerability, sanitize or validate the user-supplied artifact_id before using it in a filesystem operation. The best approach is to construct the final file path, normalize it using os.path.normpath (or .resolve() from pathlib), and then check that the final path is indeed inside the intended RESULTS_DIR directory. If the check fails, return an HTTP 400 or 403 error. This prevents attackers from using crafted artifact_id values to escape the intended directory.

What to change:

  • In test_get_size_analysis_raw, normalize the constructed path and ensure it is within the RESULTS_DIR.
  • If the normalized path does not start with/contain the desired base directory, raise an HTTP error.
  • To use os.path.normpath, ensure os is imported (which it is).
  • The fix goes in the block beginning at line 316.

Suggested changeset 1
tests/e2e/mock-sentry-api/server.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/tests/e2e/mock-sentry-api/server.py b/tests/e2e/mock-sentry-api/server.py
--- a/tests/e2e/mock-sentry-api/server.py
+++ b/tests/e2e/mock-sentry-api/server.py
@@ -315,11 +315,13 @@
 @app.get("/test/results/{artifact_id}/size-analysis-raw")
 async def test_get_size_analysis_raw(artifact_id: str):
     """Test helper: Get raw size analysis JSON."""
-    result_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"
-
+    # Build and normalize the intended path
+    result_path = (RESULTS_DIR / f"{artifact_id}_size_analysis.json").resolve()
+    # Prevent directory traversal: ensure result_path remains under RESULTS_DIR
+    if not str(result_path).startswith(str(RESULTS_DIR.resolve()) + os.sep):
+        raise HTTPException(status_code=400, detail="Invalid artifact_id")
     if not result_path.exists():
         raise HTTPException(status_code=404, detail="Size analysis not found")
-
     return JSONResponse(json.loads(result_path.read_text()))
 
 
EOF
@@ -315,11 +315,13 @@
@app.get("/test/results/{artifact_id}/size-analysis-raw")
async def test_get_size_analysis_raw(artifact_id: str):
"""Test helper: Get raw size analysis JSON."""
result_path = RESULTS_DIR / f"{artifact_id}_size_analysis.json"

# Build and normalize the intended path
result_path = (RESULTS_DIR / f"{artifact_id}_size_analysis.json").resolve()
# Prevent directory traversal: ensure result_path remains under RESULTS_DIR
if not str(result_path).startswith(str(RESULTS_DIR.resolve()) + os.sep):
raise HTTPException(status_code=400, detail="Invalid artifact_id")
if not result_path.exists():
raise HTTPException(status_code=404, detail="Size analysis not found")

return JSONResponse(json.loads(result_path.read_text()))


Copilot is powered by AI and may make mistakes. Always verify output.
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.

2 participants