-
Notifications
You must be signed in to change notification settings - Fork 14
Add APK upload/install endpoints #643
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
Conversation
| try: | ||
| result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) | ||
| result = subprocess.run( | ||
| command, |
Check failure
Code scanning / CodeQL
Uncontrolled command line Critical
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix uncontrolled command-line issues you should avoid shell=True with untrusted input and instead pass arguments as a list to subprocess.run so they are not interpreted by a shell. If shell usage is unavoidable, you must strictly validate or allowlist user input before including it in the command.
For this file, the safest change without altering functionality is:
- Refactor
run_adb_commandto accept the command as a list of arguments and invokesubprocess.runwithshell=False. - Update all callers (shown snippet:
get_devicesandcapture_ui_dump) to pass argument lists instead of format-assembled strings. - In particular, build the optional
-s <serial>portion as separate list elements, not interpolated into a shell string.
Concretely:
-
In
run_adb_command(around lines 107–121):- Change the
commandparameter to be a sequence of arguments (we can keep the name but adjust usage). - Change the
subprocess.runcall tosubprocess.run(command, shell=False, ...).
- Change the
-
In
get_devices(around line 48):- Replace
devices_output = run_adb_command(f"{ADB_PATH} devices -l")bydevices_output = run_adb_command([ADB_PATH, "devices", "-l"]).
- Replace
-
In
capture_ui_dump(around lines 123–128):- Replace construction of
device_flagas a string and the.strip()-based assembly with list concatenation, e.g.:
cmd = [ADB_PATH] if device_serial: cmd.extend(["-s", device_serial]) cmd.extend(["shell", "uiautomator", "dump", "/sdcard/ui.xml"]) out = run_adb_command(cmd)
- Replace construction of
No new imports are required; we only adjust how subprocess.run is invoked and how commands are constructed.
-
Copy modified line R48 -
Copy modified lines R61-R62 -
Copy modified lines R108-R111 -
Copy modified line R115 -
Copy modified lines R125-R129
| @@ -45,7 +45,7 @@ | ||
| """Get list of connected Android devices.""" | ||
| try: | ||
| # Get list of devices | ||
| devices_output = run_adb_command(f"{ADB_PATH} devices -l") | ||
| devices_output = run_adb_command([ADB_PATH, "devices", "-l"]) | ||
| devices = [] | ||
|
|
||
| # Parse adb devices output | ||
| @@ -58,8 +58,8 @@ | ||
| status = parts[1] | ||
| name = f"device {index}" | ||
| index += 1 | ||
| # model = run_adb_command(f"{ADB_PATH} -s {serial} shell getprop ro.product.model") | ||
| # product = run_adb_command(f"{ADB_PATH} -s {serial} shell getprop ro.product.name") | ||
| # model = run_adb_command([ADB_PATH, "-s", serial, "shell", "getprop", "ro.product.model"]) | ||
| # product = run_adb_command([ADB_PATH, "-s", serial, "shell", "getprop", "ro.product.name"]) | ||
|
|
||
| devices.append(DeviceInfo(serial=serial, status=status, name=name)) | ||
|
|
||
| @@ -105,11 +105,14 @@ | ||
|
|
||
|
|
||
| def run_adb_command(command): | ||
| """Run an ADB command and return the output.""" | ||
| """Run an ADB command and return the output. | ||
|
|
||
| The command must be provided as a list of arguments to avoid shell injection. | ||
| """ | ||
| try: | ||
| result = subprocess.run( | ||
| command, | ||
| shell=True, | ||
| shell=False, | ||
| check=True, | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| @@ -122,10 +122,11 @@ | ||
|
|
||
| def capture_ui_dump(device_serial: str | None = None): | ||
| """Capture the current UI hierarchy from the device""" | ||
| device_flag = f"-s {device_serial}" if device_serial else "" | ||
| out = run_adb_command( | ||
| f"{ADB_PATH} {device_flag} shell uiautomator dump /sdcard/ui.xml".strip() | ||
| ) | ||
| cmd = [ADB_PATH] | ||
| if device_serial: | ||
| cmd.extend(["-s", device_serial]) | ||
| cmd.extend(["shell", "uiautomator", "dump", "/sdcard/ui.xml"]) | ||
| out = run_adb_command(cmd) | ||
| if out.startswith("Error:"): | ||
| from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import ( | ||
| appium_driver, |
| def handle_apk_install(filename: str, serial: str): | ||
| dir_path = f"{ZEUZ_NODE_DOWNLOADS_DIR}/apk" | ||
| file_path = os.path.join(dir_path, filename) | ||
| if not os.path.exists(file_path): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix uncontrolled path usage, normalize the path, enforce that it stays within an expected base directory, and/or restrict names to a safe subset. Here, the safest and simplest fix without changing functionality is: (1) derive an absolute, normalized path from ZEUZ_NODE_DOWNLOADS_DIR/apk and the provided filename; (2) reject any request where the resolved path is not within that directory; and (3) optionally disallow path separators in filename so only simple filenames are accepted. This preserves the existing behavior for valid names in the apk directory while preventing traversal or absolute paths.
Concretely in server/mobile.py, update handle_apk_install:
- Compute
base_dir = os.path.abspath(os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "apk")). - Normalize the user-supplied
filenameand constructfile_path = os.path.abspath(os.path.join(base_dir, filename)). - Check
file_path.startswith(base_dir + os.sep)(or equality if ever equal to base), and if not, return an error like{"message": "Invalid filename"}. - Optionally, before path computations, enforce that
filenamedoes not contain path separators ('/'oros.sep) and is non-empty. - Keep the rest of the logic (existence check,
get_package_name,subprocess.run) unchanged.
No new external libraries are needed; os.path utilities already imported via import os are sufficient.
-
Copy modified lines R240-R246
| @@ -237,8 +237,13 @@ | ||
|
|
||
| @router.post("/apk-install") | ||
| def handle_apk_install(filename: str, serial: str): | ||
| dir_path = f"{ZEUZ_NODE_DOWNLOADS_DIR}/apk" | ||
| file_path = os.path.join(dir_path, filename) | ||
| base_dir = os.path.abspath(os.path.join(ZEUZ_NODE_DOWNLOADS_DIR, "apk")) | ||
| # Reject filenames that attempt to traverse directories or use absolute/invalid paths | ||
| if not filename or os.path.isabs(filename) or any(sep in filename for sep in (os.sep, os.altsep) if sep): | ||
| return {"message": "Invalid filename", "filename": filename} | ||
| file_path = os.path.abspath(os.path.join(base_dir, filename)) | ||
| if not (file_path == base_dir or file_path.startswith(base_dir + os.sep)): | ||
| return {"message": "Invalid filename", "filename": filename} | ||
| if not os.path.exists(file_path): | ||
| return {"message": "APK not found", "filename": filename} | ||
| package_name = get_package_name(file_path) |
| subprocess.run([ADB_PATH, "-s", serial, "install", file_path], check=True) | ||
| return {"message": "APK installed successfully", "filename": filename, "package_name": package_name} | ||
| except Exception as e: | ||
| return {"message": f"Error installing APK: {str(e)}", "filename": filename, "package_name": package_name} |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Stack trace information
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix this kind of issue you should avoid including raw exception text or stack traces in responses sent back to clients. Instead, log the full error details on the server (to logs or monitoring systems) and return a generic, user-friendly error message that does not reveal internal implementation details.
For this specific case in server/mobile.py, the best fix is:
- In
handle_apk_install, modify theexcept Exception as e:block so that:- It logs the error details using
CommonUtil.ExecLog(already used elsewhere in this file) includingstr(e)and any helpful context such asfilenameandserial. - It returns a response with a generic error message, e.g.
"Failed to install APK.", without embeddingstr(e).
- It logs the error details using
This change only affects the error path and does not alter the success path or the API shape (the JSON keys can remain "message", "filename", and "package_name"). No new imports are required; we reuse CommonUtil.ExecLog, which is already imported and used in upload_android_ui_dump.
Concretely:
- Edit the
exceptblock around lines 248–249 to:- Call
CommonUtil.ExecLog("", f"Error installing APK '{filename}' on device '{serial}': {str(e)}", iLogLevel=3). - Return
{"message": "Failed to install APK.", "filename": filename, "package_name": package_name}.
- Call
-
Copy modified lines R249-R258
| @@ -246,5 +246,14 @@ | ||
| subprocess.run([ADB_PATH, "-s", serial, "install", file_path], check=True) | ||
| return {"message": "APK installed successfully", "filename": filename, "package_name": package_name} | ||
| except Exception as e: | ||
| return {"message": f"Error installing APK: {str(e)}", "filename": filename, "package_name": package_name} | ||
| CommonUtil.ExecLog( | ||
| "", | ||
| f"Error installing APK '{filename}' on device '{serial}': {str(e)}", | ||
| iLogLevel=3, | ||
| ) | ||
| return { | ||
| "message": "Failed to install APK.", | ||
| "filename": filename, | ||
| "package_name": package_name, | ||
| } | ||
|
|
PR Type
Feature
PR Checklist
Overview
Node now able to receive request to upload android apk file. It also can install uploaded APK file.