Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 187 additions & 4 deletions server/mobile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import subprocess
import base64
import json
from typing import Literal
from typing import Literal, Optional
import asyncio
import socket
import xml.etree.ElementTree as ET
import zipfile
import plistlib

import requests
from androguard.core.apk import APK
Expand Down Expand Up @@ -253,7 +255,7 @@

def run_adb_command(command):
"""Run an ADB command and return the output."""
try:

Check failure

Code scanning / CodeQL

Uncontrolled command line Critical

This command line depends on a
user-provided value
.
This command line depends on a
user-provided value
.
This command line depends on a user-provided value.
result = subprocess.run(
command,
shell=True,
Expand Down Expand Up @@ -404,9 +406,6 @@
return
except:
pass

# No real source available
raise Exception("iOS service error. Make sure simulator is running.")


async def upload_android_ui_dump():
Expand Down Expand Up @@ -533,3 +532,187 @@
except Exception as e:
return {"message": f"Error installing APK: {str(e)}", "filename": filename, "package_name": package_name}


@router.get("/package-installed")
def check_package_installed(package_name: str, serial: str | None = None):
"""Check if an android package is installed on the device."""
try:
device_flag = f"-s {serial}" if serial else ""
result = run_adb_command(
f"{ADB_PATH} {device_flag} shell pm list packages {package_name}".strip()
)
if result.startswith("Error:"):
return {"installed": False, "package_name": package_name, "error": result}

# pm list packages returns lines like "package:com.example.app"
# Check if the exact package is in the output
installed_packages = [line.replace("package:", "") for line in result.split("\n") if line.startswith("package:")]
is_installed = package_name in installed_packages

return {"installed": is_installed, "package_name": package_name}
except Exception as e:
return {"installed": False, "package_name": package_name, "error": str(e)}


@router.post("/ios/app-upload")
def handle_ios_app_upload(file: UploadFile = File(...)):
dir_path = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
if not os.path.exists(dir_path):
os.makedirs(dir_path)

filename = file.filename or "uploaded.app"
filepath = os.path.join(dir_path, filename)
with open(filepath, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)

return {"message": "App uploaded successfully", "filename": filename}


def normalized_ios_app_path(file_path: str) -> Optional[str]:
"""
ensure that we ended up with a .app directory even if user provided .ipa
"""
if not os.path.exists(file_path):
return None

# if already .app
if file_path.endswith(".app") and os.path.isdir(file_path):
return file_path

# .ipa, so we have to extract to get .app
if file_path.endswith(".ipa"):
extract_dir = file_path.replace(".ipa", "_extracted")
zip_path = file_path.replace(".ipa", ".zip")

# copy instead of rename to avoid breaking original file
shutil.copy(file_path, zip_path)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 12 days ago

In general, to fix this kind of issue you must sanitize or validate any user-controlled path component before using it in filesystem operations. Common strategies are to (1) normalize the path and ensure it remains under a trusted root directory, and/or (2) restrict filenames to a safe subset (no path separators, no .., and limited extensions).

In this specific case, the best fix without changing functionality is:

  1. Constrain filename in /ios/app-install so it cannot contain directory traversal or absolute paths and can only refer to files under ZEUZ_NODE_DOWNLOADS_DIR/ios-app.
  2. Normalize the resulting filepath with os.path.realpath and confirm that it still resides under the intended base directory before using it.
  3. Optionally, centralize the check in a small helper to keep the logic clear, but we can also inline it where needed.

Concretely:

  • In handle_ios_app_install, after receiving filename, validate it:
    • Reject if it is empty, contains os.sep / os.altsep, or ...
  • Build filepath using os.path.join(dirpath, filename), then normalize with os.path.realpath.
  • Check that filepath starts with os.path.realpath(dirpath) + os.sep (or equals the base dir in edge cases); if not, return an error.
  • Use this validated filepath for the existence check and subsequent call to normalized_ios_app_path.

This keeps the rest of the logic (unzipping, extracting bundle IDs, installing to the simulator) unchanged while removing the ability for a caller to point the code at arbitrary files elsewhere on the filesystem.

Suggested changeset 1
server/mobile.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/server/mobile.py b/server/mobile.py
--- a/server/mobile.py
+++ b/server/mobile.py
@@ -642,7 +642,17 @@
     handling the ios app installation in the simolator
     """
     dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
-    filepath = os.path.join(dirpath, filename)
+    # Validate filename to prevent path traversal and ensure it stays under dirpath
+    if not filename:
+        return {"message": "Invalid filename", "filename": filename}
+    # Disallow path separators and parent directory references in the filename
+    if os.path.sep in filename or (os.path.altsep and os.path.altsep in filename) or ".." in filename:
+        return {"message": "Invalid filename", "filename": filename}
+    # Build and normalize the full path, then ensure it is within dirpath
+    base_dir_real = os.path.realpath(dirpath)
+    filepath = os.path.realpath(os.path.join(base_dir_real, filename))
+    if not (filepath == base_dir_real or filepath.startswith(base_dir_real + os.path.sep)):
+        return {"message": "Invalid filename", "filename": filename}
     if not os.path.exists(filepath):
         return {"message": "App not found", "filename": filename}
     
EOF
@@ -642,7 +642,17 @@
handling the ios app installation in the simolator
"""
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
# Validate filename to prevent path traversal and ensure it stays under dirpath
if not filename:
return {"message": "Invalid filename", "filename": filename}
# Disallow path separators and parent directory references in the filename
if os.path.sep in filename or (os.path.altsep and os.path.altsep in filename) or ".." in filename:
return {"message": "Invalid filename", "filename": filename}
# Build and normalize the full path, then ensure it is within dirpath
base_dir_real = os.path.realpath(dirpath)
filepath = os.path.realpath(os.path.join(base_dir_real, filename))
if not (filepath == base_dir_real or filepath.startswith(base_dir_real + os.path.sep)):
return {"message": "Invalid filename", "filename": filename}
if not os.path.exists(filepath):
return {"message": "App not found", "filename": filename}

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 12 days ago

In general, to fix uncontrolled path usage you must ensure that any path derived from user input is (1) restricted to a safe base directory and (2) normalized before comparison. That means building a candidate path, normalizing it (e.g., with os.path.realpath or os.path.normpath), and then verifying that the resulting absolute path is still under the intended root directory. If the check fails, reject the request.

For this specific code, the main risk is that filename in handle_ios_app_install can contain .. or an absolute path, leading to filepath pointing outside ZEUZ_NODE_DOWNLOADS_DIR/ios-app. normalized_ios_app_path then happily operates on whatever file_path it receives, including copying and extracting files from arbitrary locations. The minimal, behavior-preserving fix is to validate and normalize filepath in handle_ios_app_install immediately after joining, and before checking existence or passing it to normalized_ios_app_path. We can use os.path.realpath to normalize, compute a normalized dirpath as the trusted root, and then ensure that the resultant filepath is inside dirpath (e.g., by using os.path.commonpath). If the check fails, return an error without touching the filesystem. This keeps all existing behavior intact for legitimate filenames while preventing traversal.

Concretely:

  • In handle_ios_app_install, after computing dirpath and the initial filepath with os.path.join(dirpath, filename), add:
    • dirpath = os.path.realpath(dirpath)
    • filepath = os.path.realpath(filepath)
    • a guard: if os.path.commonpath([dirpath, filepath]) != dirpath: return {"message": "Invalid app path", "filename": filename}
  • Keep the subsequent os.path.exists(filepath) and calls as-is; now they only apply to vetted paths.
  • No changes are needed inside normalized_ios_app_path for this particular taint path, since file_path will already be normalized and constrained.

No new third-party imports are required; we only rely on the existing os module.


Suggested changeset 1
server/mobile.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/server/mobile.py b/server/mobile.py
--- a/server/mobile.py
+++ b/server/mobile.py
@@ -643,6 +643,11 @@
     """
     dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
     filepath = os.path.join(dirpath, filename)
+    # Normalize and ensure the app path stays within the expected directory
+    dirpath = os.path.realpath(dirpath)
+    filepath = os.path.realpath(filepath)
+    if os.path.commonpath([dirpath, filepath]) != dirpath:
+        return {"message": "Invalid app path", "filename": filename}
     if not os.path.exists(filepath):
         return {"message": "App not found", "filename": filename}
     
EOF
@@ -643,6 +643,11 @@
"""
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
# Normalize and ensure the app path stays within the expected directory
dirpath = os.path.realpath(dirpath)
filepath = os.path.realpath(filepath)
if os.path.commonpath([dirpath, filepath]) != dirpath:
return {"message": "Invalid app path", "filename": filename}
if not os.path.exists(filepath):
return {"message": "App not found", "filename": filename}

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

with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(extract_dir)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 12 days ago

In general, to fix uncontrolled path usage, we need to (1) normalize any paths built from user input, (2) ensure they remain within a trusted root directory, and (3) avoid using raw filenames directly as directories or extraction roots without validation. For archive extraction, we must additionally guard against path traversal inside the archive by checking each member’s path before extraction.

For this code, the best targeted fix without changing existing behavior is:

  1. Constrain filename to the ios-app directory in /ios/app-install:

    • Build filepath from ZEUZ_NODE_DOWNLOADS_DIR/ios-app and filename.
    • Normalize it with os.path.normpath.
    • Ensure the normalized path is still under the intended root directory; if not, reject.
  2. Constrain derived directories inside normalized_ios_app_path:

    • Treat file_path as trusted only if it is under the same safe root. Since we cannot rely on external context inside this helper, we can:
      • Add an internal base directory for iOS apps (os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app")) and ensure any temporary directories (extract_dir, etc.) also live under that base.
      • Normalize extract_dir (for .ipa, .zip, .app.zip cases) and verify it starts with that base directory.
  3. Harden archive extraction:

    • Instead of zip_ref.extractall(extract_dir), iterate over zip_ref.infolist(), build each output path, normalize it, and verify it still starts with extract_dir before writing. This prevents malicious entries like ../../etc/passwd.

Implementation details:

  • In handle_ios_app_install, replace the simple join with a normpath+prefix check:

    dirpath = os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app")
    safe_dirpath = os.path.abspath(dirpath)
    requested_path = os.path.abspath(os.path.normpath(os.path.join(safe_dirpath, filename)))
    if not requested_path.startswith(safe_dirpath + os.sep):
        return {"message": "Invalid filename", "filename": filename}
    filepath = requested_path
  • In normalized_ios_app_path, use ZEUZ_NODE_DOWNLOADS_DIR to compute an ios base directory and apply:

    • An early check that file_path is under that base (using os.path.abspath + startswith).
    • When building extract_dir for .ipa and .zip cases, ensure its normalized absolute path is also under the same base.
    • Replace extractall(extract_dir) usages with a safe_extract loop that enforces the extract_dir prefix for each member.

We can reuse the existing os and zipfile imports; no new external libraries are required.

Suggested changeset 1
server/mobile.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/server/mobile.py b/server/mobile.py
--- a/server/mobile.py
+++ b/server/mobile.py
@@ -574,21 +574,47 @@
     """
     if not os.path.exists(file_path):
         return None
+
+    # ensure the file path is under the ios-app downloads directory
+    ios_base_dir = os.path.abspath(os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app"))
+    abs_file_path = os.path.abspath(os.path.normpath(file_path))
+    if not abs_file_path.startswith(ios_base_dir + os.sep):
+        return None
     
     # if already .app
-    if file_path.endswith(".app") and os.path.isdir(file_path):
-        return file_path
+    if abs_file_path.endswith(".app") and os.path.isdir(abs_file_path):
+        return abs_file_path
+
+    def _safe_extract_all(zip_ref: zipfile.ZipFile, extract_dir: str) -> None:
+        """
+        Safely extract all members of zip_ref into extract_dir, preventing path traversal.
+        """
+        abs_extract_dir = os.path.abspath(extract_dir)
+        for member in zip_ref.infolist():
+            member_path = os.path.abspath(os.path.join(abs_extract_dir, member.filename))
+            if not member_path.startswith(abs_extract_dir + os.sep):
+                # skip files that would escape the extraction directory
+                continue
+            if member.is_dir():
+                os.makedirs(member_path, exist_ok=True)
+            else:
+                os.makedirs(os.path.dirname(member_path), exist_ok=True)
+                with zip_ref.open(member, "r") as source, open(member_path, "wb") as target:
+                    shutil.copyfileobj(source, target)
     
     # .ipa, so we have to extract to get .app
-    if file_path.endswith(".ipa"):
-        extract_dir = file_path.replace(".ipa", "_extracted")
-        zip_path = file_path.replace(".ipa", ".zip")
+    if abs_file_path.endswith(".ipa"):
+        extract_dir_candidate = abs_file_path.replace(".ipa", "_extracted")
+        extract_dir = os.path.abspath(os.path.normpath(extract_dir_candidate))
+        if not extract_dir.startswith(ios_base_dir + os.sep):
+            return None
+        zip_path = abs_file_path.replace(".ipa", ".zip")
 
         # copy instead of rename to avoid breaking original file
-        shutil.copy(file_path, zip_path)
+        shutil.copy(abs_file_path, zip_path)
         
         with zipfile.ZipFile(zip_path, "r") as zip_ref:
-            zip_ref.extractall(extract_dir)
+            _safe_extract_all(zip_ref, extract_dir)
             
         payload_dir = os.path.join(extract_dir, "Payload")
         if not os.path.isdir(payload_dir):
@@ -596,23 +614,30 @@
         
         for item in os.listdir(payload_dir):
             if item.endswith(".app"):
-                return os.path.join(payload_dir, item)
+                app_dir = os.path.join(payload_dir, item)
+                if os.path.isdir(app_dir):
+                    return app_dir
         
         return None
     
     # extract zip because 'test.app' is not a file, so browser zip that before send to me
-    if file_path.endswith(".app.zip") or file_path.endswith(".zip"):
-        extract_dir = file_path + "_extracted"
+    if abs_file_path.endswith(".app.zip") or abs_file_path.endswith(".zip"):
+        extract_dir_candidate = abs_file_path + "_extracted"
+        extract_dir = os.path.abspath(os.path.normpath(extract_dir_candidate))
+        if not extract_dir.startswith(ios_base_dir + os.sep):
+            return None
         os.makedirs(extract_dir, exist_ok=True)
 
-        with zipfile.ZipFile(file_path, "r") as zip_ref:
-            zip_ref.extractall(extract_dir)
+        with zipfile.ZipFile(abs_file_path, "r") as zip_ref:
+            _safe_extract_all(zip_ref, extract_dir)
 
         # search recursively for .app folder so it will pick very first .app folder
         for root, dirs, files in os.walk(extract_dir):
             for d in dirs:
-                if d.endswith(".app") and os.path.isdir(os.path.join(root, d)):
-                    return os.path.join(root, d)
+                if d.endswith(".app"):
+                    app_dir = os.path.join(root, d)
+                    if os.path.isdir(app_dir):
+                        return app_dir
 
         return None
 
@@ -641,8 +656,13 @@
     """
     handling the ios app installation in the simolator
     """
-    dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
-    filepath = os.path.join(dirpath, filename)
+    dirpath = os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app")
+    base_dirpath = os.path.abspath(dirpath)
+    # Normalize the requested path and ensure it stays within the ios-app directory
+    requested_path = os.path.abspath(os.path.normpath(os.path.join(base_dirpath, filename)))
+    if not requested_path.startswith(base_dirpath + os.sep):
+        return {"message": "Invalid filename", "filename": filename}
+    filepath = requested_path
     if not os.path.exists(filepath):
         return {"message": "App not found", "filename": filename}
     
EOF
Copilot is powered by AI and may make mistakes. Always verify output.

payload_dir = os.path.join(extract_dir, "Payload")
if not os.path.isdir(payload_dir):

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 12 days ago

In general, the fix is to ensure that any path derived from user-controlled data is constrained to a safe root directory, and normalized before use. For this endpoint, we must guarantee that filename always resolves to a file inside ZEUZ_NODE_DOWNLOADS_DIR/ios-app, and reject or sanitize values that attempt path traversal or use absolute paths. After that, subsequent operations (normalized_ios_app_path, extraction, payload_dir, etc.) will operate only within that safe area.

The best targeted fix without changing existing functionality is:

  1. In handle_ios_app_install, normalize the constructed path and enforce that it remains under the intended directory:
    • Construct dirpath = os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app") (safer than string interpolation).
    • Construct requested_path = os.path.join(dirpath, filename).
    • Normalize with os.path.normpath and reject if:
      • The normalized path is not within dirpath (prefix check on absolute paths), or
      • The normalized path is a directory traversal (e.g., it does not start with os.path.abspath(dirpath) + os.sep).
  2. Use the validated path (filepath) for subsequent logic.
  3. Optionally, ensure dirpath exists with os.makedirs(dirpath, exist_ok=True) so that normalization and existence checks work as expected.

All required functions (os.path.join, os.path.normpath, os.path.abspath, os.makedirs) are already available through the existing import os, so no new imports are necessary. The change is localized to the block around lines 640–647 in server/mobile.py.


Suggested changeset 1
server/mobile.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/server/mobile.py b/server/mobile.py
--- a/server/mobile.py
+++ b/server/mobile.py
@@ -641,8 +641,15 @@
     """
     handling the ios app installation in the simolator
     """
-    dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
-    filepath = os.path.join(dirpath, filename)
+    dirpath = os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app")
+    # Normalize and validate the requested path to prevent directory traversal
+    requested_path = os.path.normpath(os.path.join(dirpath, filename))
+    dirpath_abs = os.path.abspath(dirpath)
+    requested_path_abs = os.path.abspath(requested_path)
+    if not requested_path_abs.startswith(dirpath_abs + os.sep):
+        return {"message": "Invalid filename", "filename": filename}
+
+    filepath = requested_path_abs
     if not os.path.exists(filepath):
         return {"message": "App not found", "filename": filename}
     
EOF
@@ -641,8 +641,15 @@
"""
handling the ios app installation in the simolator
"""
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
dirpath = os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app")
# Normalize and validate the requested path to prevent directory traversal
requested_path = os.path.normpath(os.path.join(dirpath, filename))
dirpath_abs = os.path.abspath(dirpath)
requested_path_abs = os.path.abspath(requested_path)
if not requested_path_abs.startswith(dirpath_abs + os.sep):
return {"message": "Invalid filename", "filename": filename}

filepath = requested_path_abs
if not os.path.exists(filepath):
return {"message": "App not found", "filename": filename}

Copilot is powered by AI and may make mistakes. Always verify output.
return None

for item in os.listdir(payload_dir):

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 12 days ago

In general, to fix this type of issue you must (1) constrain user-controlled filenames to a specific directory and prevent path traversal, and (2) when extracting archives, ensure that no file in the archive is written outside the intended extraction directory. This is usually done by normalizing paths with os.path.abspath / os.path.normpath, checking that they start with a trusted base directory, and validating or rejecting unsafe inputs.

For this specific code, the best fix with minimal functional change consists of two parts:

  1. Constrain filename and filepath in handle_ios_app_install:

    • After computing dirpath and filepath, normalize filepath (for example with os.path.abspath(os.path.join(dirpath, filename))).
    • Check that the resulting absolute path is still under dirpath (for instance by using os.path.commonpath or prefix comparison). If it is not, return an error (e.g. “Invalid filename”) instead of proceeding.
    • Use this validated filepath for subsequent operations and pass it into normalized_ios_app_path.
  2. Harden archive extraction in normalized_ios_app_path:

    • When building extract_dir and zip_path from file_path, normalize them as well using os.path.abspath so they are absolute paths under a directory derived from the trusted file_path.
    • Replace the direct zip_ref.extractall(extract_dir) call with a “safe extract” loop that inspects every member name, joins it with extract_dir, normalizes it, and verifies that it remains inside extract_dir. If any member would be extracted outside the directory (a zip-slip attack using ../ or absolute paths), raise an error or return None instead of extracting.
    • This keeps the existing behavior of extracting the IPA to get the .app bundle, but prevents the archive from placing files arbitrarily on the filesystem.

All of these changes can be implemented in server/mobile.py using the existing os and zipfile imports; no new external dependencies are required. The edits will occur around lines 584–591 in normalized_ios_app_path and lines 644–649 in handle_ios_app_install.

Suggested changeset 1
server/mobile.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/server/mobile.py b/server/mobile.py
--- a/server/mobile.py
+++ b/server/mobile.py
@@ -581,23 +581,29 @@
     
     # .ipa, so we have to extract to get .app
     if file_path.endswith(".ipa"):
-        extract_dir = file_path.replace(".ipa", "_extracted")
-        zip_path = file_path.replace(".ipa", ".zip")
+        extract_dir = os.path.abspath(file_path.replace(".ipa", "_extracted"))
+        zip_path = os.path.abspath(file_path.replace(".ipa", ".zip"))
 
         # copy instead of rename to avoid breaking original file
         shutil.copy(file_path, zip_path)
-        
+
+        # Safely extract the IPA to avoid writing files outside extract_dir
         with zipfile.ZipFile(zip_path, "r") as zip_ref:
+            for member in zip_ref.namelist():
+                member_target_path = os.path.abspath(os.path.join(extract_dir, member))
+                if not member_target_path.startswith(extract_dir + os.sep):
+                    # Unsafe path detected inside archive
+                    return None
             zip_ref.extractall(extract_dir)
-            
+
         payload_dir = os.path.join(extract_dir, "Payload")
         if not os.path.isdir(payload_dir):
             return None
-        
+
         for item in os.listdir(payload_dir):
             if item.endswith(".app"):
                 return os.path.join(payload_dir, item)
-        
+
         return None
     
     # extract zip because 'test.app' is not a file, so browser zip that before send to me
@@ -642,10 +637,13 @@
     handling the ios app installation in the simolator
     """
     dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
-    filepath = os.path.join(dirpath, filename)
+    # Normalize and validate the requested filename to prevent path traversal
+    filepath = os.path.abspath(os.path.join(dirpath, filename))
+    if not filepath.startswith(os.path.abspath(dirpath) + os.sep):
+        return {"message": "Invalid filename", "filename": filename}
     if not os.path.exists(filepath):
         return {"message": "App not found", "filename": filename}
-    
+
     app_path = normalized_ios_app_path(filepath)
     if not app_path:
         return {"message": "Failed to normalize .app", "filename": filename}
EOF
@@ -581,23 +581,29 @@

# .ipa, so we have to extract to get .app
if file_path.endswith(".ipa"):
extract_dir = file_path.replace(".ipa", "_extracted")
zip_path = file_path.replace(".ipa", ".zip")
extract_dir = os.path.abspath(file_path.replace(".ipa", "_extracted"))
zip_path = os.path.abspath(file_path.replace(".ipa", ".zip"))

# copy instead of rename to avoid breaking original file
shutil.copy(file_path, zip_path)


# Safely extract the IPA to avoid writing files outside extract_dir
with zipfile.ZipFile(zip_path, "r") as zip_ref:
for member in zip_ref.namelist():
member_target_path = os.path.abspath(os.path.join(extract_dir, member))
if not member_target_path.startswith(extract_dir + os.sep):
# Unsafe path detected inside archive
return None
zip_ref.extractall(extract_dir)

payload_dir = os.path.join(extract_dir, "Payload")
if not os.path.isdir(payload_dir):
return None

for item in os.listdir(payload_dir):
if item.endswith(".app"):
return os.path.join(payload_dir, item)

return None

# extract zip because 'test.app' is not a file, so browser zip that before send to me
@@ -642,10 +637,13 @@
handling the ios app installation in the simolator
"""
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
# Normalize and validate the requested filename to prevent path traversal
filepath = os.path.abspath(os.path.join(dirpath, filename))
if not filepath.startswith(os.path.abspath(dirpath) + os.sep):
return {"message": "Invalid filename", "filename": filename}
if not os.path.exists(filepath):
return {"message": "App not found", "filename": filename}

app_path = normalized_ios_app_path(filepath)
if not app_path:
return {"message": "Failed to normalize .app", "filename": filename}
Copilot is powered by AI and may make mistakes. Always verify output.
if item.endswith(".app"):
return os.path.join(payload_dir, item)

return None

# extract zip because 'test.app' is not a file, so browser zip that before send to me
if file_path.endswith(".app.zip") or file_path.endswith(".zip"):
extract_dir = file_path + "_extracted"
os.makedirs(extract_dir, exist_ok=True)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 12 days ago

To fix this, filename must be validated and constrained so that all filesystem operations stay within the intended downloads directory. The safest general pattern here is to: (1) build an absolute path rooted at ZEUZ_NODE_DOWNLOADS_DIR/ios-app, (2) normalize it with os.path.abspath or os.path.realpath, and (3) ensure the normalized path is inside that base directory (for example by checking that it starts with the normalized base path plus a path separator). If the check fails, reject the request. This prevents ../ traversal and absolute path tricks.

The single best change, without altering existing functionality, is to introduce a small helper that takes the untrusted filename, joins it with the known-safe directory, normalizes the result, and verifies that the resulting path remains under the base directory. Then use this helper in /ios/app-install instead of the raw os.path.join. Concretely, within server/mobile.py, add a function like get_safe_ios_app_filepath(filename: str) -> Optional[str] near the other iOS helpers (normalized_ios_app_path, extract_bundle_id_from_app) that: computes base_dir = os.path.abspath(os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app")), then candidate = os.path.abspath(os.path.join(base_dir, filename)), then checks that os.path.commonpath([base_dir, candidate]) == base_dir. If the check passes and the file exists, return candidate; otherwise return None. Then in handle_ios_app_install, replace the existing construction and existence check:

dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
if not os.path.exists(filepath):
    ...

with a call to the helper:

filepath = get_safe_ios_app_filepath(filename)
if filepath is None:
    ...

No new imports are needed because os is already imported. All subsequent code that uses filepath and file_path (inside normalized_ios_app_path) will now only operate on safe, normalized paths under the intended directory; extract_dir becomes derived from a validated base path, eliminating the uncontrolled path expression.

Suggested changeset 1
server/mobile.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/server/mobile.py b/server/mobile.py
--- a/server/mobile.py
+++ b/server/mobile.py
@@ -568,6 +568,30 @@
     return {"message": "App uploaded successfully", "filename": filename}
 
 
+def get_safe_ios_app_filepath(filename: str) -> Optional[str]:
+    """
+    Resolve a user-supplied filename to a path within the ios-app downloads directory.
+    Returns the absolute path if it is within the base directory and exists, otherwise None.
+    """
+    base_dir = os.path.abspath(os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app"))
+    candidate = os.path.abspath(os.path.join(base_dir, filename))
+
+    # Ensure the resolved path is within the intended base directory
+    try:
+        common = os.path.commonpath([base_dir, candidate])
+    except ValueError:
+        # Different drives on Windows or invalid paths
+        return None
+
+    if common != base_dir:
+        return None
+
+    if not os.path.exists(candidate):
+        return None
+
+    return candidate
+
+
 def normalized_ios_app_path(file_path: str) -> Optional[str]:
     """
     ensure that we ended up with a .app directory even if user provided .ipa
@@ -641,9 +665,8 @@
     """
     handling the ios app installation in the simolator
     """
-    dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
-    filepath = os.path.join(dirpath, filename)
-    if not os.path.exists(filepath):
+    filepath = get_safe_ios_app_filepath(filename)
+    if filepath is None:
         return {"message": "App not found", "filename": filename}
     
     app_path = normalized_ios_app_path(filepath)
EOF
@@ -568,6 +568,30 @@
return {"message": "App uploaded successfully", "filename": filename}


def get_safe_ios_app_filepath(filename: str) -> Optional[str]:
"""
Resolve a user-supplied filename to a path within the ios-app downloads directory.
Returns the absolute path if it is within the base directory and exists, otherwise None.
"""
base_dir = os.path.abspath(os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "ios-app"))
candidate = os.path.abspath(os.path.join(base_dir, filename))

# Ensure the resolved path is within the intended base directory
try:
common = os.path.commonpath([base_dir, candidate])
except ValueError:
# Different drives on Windows or invalid paths
return None

if common != base_dir:
return None

if not os.path.exists(candidate):
return None

return candidate


def normalized_ios_app_path(file_path: str) -> Optional[str]:
"""
ensure that we ended up with a .app directory even if user provided .ipa
@@ -641,9 +665,8 @@
"""
handling the ios app installation in the simolator
"""
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
if not os.path.exists(filepath):
filepath = get_safe_ios_app_filepath(filename)
if filepath is None:
return {"message": "App not found", "filename": filename}

app_path = normalized_ios_app_path(filepath)
Copilot is powered by AI and may make mistakes. Always verify output.

with zipfile.ZipFile(file_path, "r") as zip_ref:
zip_ref.extractall(extract_dir)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 12 days ago

In general, to fix uncontrolled-path issues you must normalize and validate any user-provided path before using it for filesystem access. When working within a specific root directory, a common pattern is: join the root and user input, normalize (e.g. os.path.realpath or os.path.normpath), and then verify that the result is still inside the intended root (e.g. using os.path.commonpath or a prefix check). Reject or error out if the validation fails.

For this codebase, the best fix without changing existing functionality is:

  1. Add a small helper that, given a base directory and a (possibly user-supplied) relative name, returns a safe, normalized path inside that base directory or None if the input escapes the base.
  2. Use this helper in both handle_ios_app_upload and handle_ios_app_install:
    • For uploads (handle_ios_app_upload), sanitize file.filename (or default) to a safe name, so that even a malicious filename cannot write outside ZEUZ_NODE_DOWNLOADS_DIR/ios-app. We should also reject path separators and normalize the filename.
    • For installs (handle_ios_app_install), validate and normalize filename against dirpath before building filepath. If invalid, return an error instead of proceeding. If valid, use the validated path for subsequent operations.
  3. Keep the rest of the logic (unzipping, extracting bundle IDs, etc.) unchanged; we only tighten how the initial filepath is derived from tainted input.

Concretely, within server/mobile.py:

  • Define a helper (e.g. def safe_join(base: str, name: str) -> Optional[str]:) above the affected handlers. It will:
    • Strip leading path separators from name.
    • Join with base.
    • Normalize with os.path.realpath (or normpath).
    • Verify that the result is under base using os.path.commonpath([base, candidate]) == os.path.realpath(base).
  • In handle_ios_app_upload:
    • Build a sanitized filename by taking only the basename (os.path.basename) and possibly falling back to "uploaded.app" if empty, then pass it through safe_join.
    • If safe_join returns None, reject the upload with an error message.
    • Use the safe path for file writing.
  • In handle_ios_app_install:
    • Use safe_join(dirpath, filename) to build filepath.
    • If None, return an error that the filename is invalid.
    • Use this filepath for the remainder of the function.

This directly addresses the identified taint flow: after the change, extract_dir ultimately depends only on a path that is guaranteed to live under ZEUZ_NODE_DOWNLOADS_DIR/ios-app, so even zip extraction cannot escape that directory tree.

Suggested changeset 1
server/mobile.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/server/mobile.py b/server/mobile.py
--- a/server/mobile.py
+++ b/server/mobile.py
@@ -25,6 +25,32 @@
 
 sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'Framework', 'Built_In_Automation', 'Mobile', 'CrossPlatform', 'Appium'))
 ADB_PATH = "adb"  # Ensure ADB is in PATH
+
+
+def safe_join(base_dir: str, name: str) -> Optional[str]:
+    """
+    Safely join a base directory and a user-supplied name, ensuring the result
+    stays within the base directory. Returns None if the path would escape.
+    """
+    # Normalize base directory
+    base_dir_real = os.path.realpath(base_dir)
+
+    # Disallow absolute paths in name
+    # Also strip any leading path separators to avoid os.path.join ignoring base_dir
+    name = name.lstrip("/\\")
+
+    candidate = os.path.realpath(os.path.join(base_dir_real, name))
+
+    try:
+        common = os.path.commonpath([base_dir_real, candidate])
+    except ValueError:
+        # Different drives on Windows or similar issues
+        return None
+
+    if common != base_dir_real:
+        return None
+
+    return candidate
 UI_XML_PATH = "ui.xml"
 SCREENSHOT_PATH = "screen.png"
 IOS_SCREENSHOT_PATH = "ios_screen.png"
@@ -560,12 +586,16 @@
     if not os.path.exists(dir_path):
         os.makedirs(dir_path)
         
-    filename = file.filename or "uploaded.app"
-    filepath = os.path.join(dir_path, filename)
+    original_name = file.filename or "uploaded.app"
+    # Use only the basename to avoid any directory components
+    safe_name = os.path.basename(original_name) or "uploaded.app"
+    filepath = safe_join(dir_path, safe_name)
+    if filepath is None:
+        return {"message": "Invalid filename", "filename": original_name}
     with open(filepath, "wb") as buffer:
         shutil.copyfileobj(file.file, buffer)
         
-    return {"message": "App uploaded successfully", "filename": filename}
+    return {"message": "App uploaded successfully", "filename": safe_name}
 
 
 def normalized_ios_app_path(file_path: str) -> Optional[str]:
@@ -642,7 +669,11 @@
     handling the ios app installation in the simolator
     """
     dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
-    filepath = os.path.join(dirpath, filename)
+    # Ensure the filename resolves to a path inside dirpath
+    safe_path = safe_join(dirpath, filename)
+    if safe_path is None:
+        return {"message": "Invalid filename", "filename": filename}
+    filepath = safe_path
     if not os.path.exists(filepath):
         return {"message": "App not found", "filename": filename}
     
EOF
@@ -25,6 +25,32 @@

sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'Framework', 'Built_In_Automation', 'Mobile', 'CrossPlatform', 'Appium'))
ADB_PATH = "adb" # Ensure ADB is in PATH


def safe_join(base_dir: str, name: str) -> Optional[str]:
"""
Safely join a base directory and a user-supplied name, ensuring the result
stays within the base directory. Returns None if the path would escape.
"""
# Normalize base directory
base_dir_real = os.path.realpath(base_dir)

# Disallow absolute paths in name
# Also strip any leading path separators to avoid os.path.join ignoring base_dir
name = name.lstrip("/\\")

candidate = os.path.realpath(os.path.join(base_dir_real, name))

try:
common = os.path.commonpath([base_dir_real, candidate])
except ValueError:
# Different drives on Windows or similar issues
return None

if common != base_dir_real:
return None

return candidate
UI_XML_PATH = "ui.xml"
SCREENSHOT_PATH = "screen.png"
IOS_SCREENSHOT_PATH = "ios_screen.png"
@@ -560,12 +586,16 @@
if not os.path.exists(dir_path):
os.makedirs(dir_path)

filename = file.filename or "uploaded.app"
filepath = os.path.join(dir_path, filename)
original_name = file.filename or "uploaded.app"
# Use only the basename to avoid any directory components
safe_name = os.path.basename(original_name) or "uploaded.app"
filepath = safe_join(dir_path, safe_name)
if filepath is None:
return {"message": "Invalid filename", "filename": original_name}
with open(filepath, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)

return {"message": "App uploaded successfully", "filename": filename}
return {"message": "App uploaded successfully", "filename": safe_name}


def normalized_ios_app_path(file_path: str) -> Optional[str]:
@@ -642,7 +669,11 @@
handling the ios app installation in the simolator
"""
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
# Ensure the filename resolves to a path inside dirpath
safe_path = safe_join(dirpath, filename)
if safe_path is None:
return {"message": "Invalid filename", "filename": filename}
filepath = safe_path
if not os.path.exists(filepath):
return {"message": "App not found", "filename": filename}

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

# search recursively for .app folder so it will pick very first .app folder
for root, dirs, files in os.walk(extract_dir):
for d in dirs:
if d.endswith(".app") and os.path.isdir(os.path.join(root, d)):
return os.path.join(root, d)

return None

return None


def extract_bundle_id_from_app(app_path: str) -> Optional[str]:
"""
Reads CFBundleIdentifier from Info.plist inside .app bundle
"""
info_plist_path = os.path.join(app_path, "Info.plist")

if not os.path.exists(info_plist_path):
return None

try:
with open(info_plist_path, "rb") as f:
plist_data = plistlib.load(f)
return plist_data.get("CFBundleIdentifier")
except Exception:
return None


@router.post("/ios/app-install")
def handle_ios_app_install(filename: str, sim_udid: str):
"""
handling the ios app installation in the simolator
"""
dirpath = f"{ZEUZ_NODE_DOWNLOADS_DIR}/ios-app"
filepath = os.path.join(dirpath, filename)
if not os.path.exists(filepath):
return {"message": "App not found", "filename": filename}

app_path = normalized_ios_app_path(filepath)
if not app_path:
return {"message": "Failed to normalize .app", "filename": filename}

bundle_id = extract_bundle_id_from_app(app_path)

try:
# uninstall if already exists
try:
subprocess.run(
["xcrun", "simctl", "uninstall", sim_udid, bundle_id],
capture_output=True, text=True, timeout=30
)
except:
pass # Ignore uninstall errors

# Install the app
result = subprocess.run(
["xcrun", "simctl", "install", sim_udid, app_path],
capture_output=True, text=True, check=True, timeout=120
)

# Verify installation
verify_result = subprocess.run(
["xcrun", "simctl", "get_app_container", sim_udid, bundle_id],
capture_output=True, text=True, timeout=30
)

if verify_result.returncode != 0:
return {
"message": f"App installed but verification failed: {verify_result.stderr}",
"filename": filename,
"bundle_id": bundle_id,
}

return {
"message": "App installed successfully",
"filename": filename,
"bundle_id": bundle_id,
}
except subprocess.CalledProcessError as e:
return {
"message": f"Error installing app: {e.stderr or str(e)}",
"filename": filename,
"bundle_id": bundle_id,
}


@router.get("/ios/bundle-installed")
def is_ios_app_installed(sim_udid: str, bundle_id: str):
try:
result = subprocess.run(
["xcrun", "simctl", "get_app_container", sim_udid, bundle_id],
capture_output=True, text=True, timeout=10
)

if result.returncode == 0 and result.stdout.strip():
list_result = subprocess.run(
["xcrun", "simctl", "listapps", sim_udid],
capture_output=True, text=True, timeout=10
)

if list_result.returncode == 0:
return {"installed": bundle_id in list_result.stdout}

return {"installed": True}

return {"installed": False}
except Exception as e:
return {"installed": False, "error": str(e)}
Loading