Skip to content
Open
Show file tree
Hide file tree
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ A universal command-line tool for managing iOS and Android devices, simulators,
- **Multiple Output Formats**: Save screenshots as PNG or JPEG with quality control
- **Screencapture video streaming**: Stream mjpeg/h264 video directly from device
- **Device Control**: Reboot devices, tap screen coordinates, press hardware buttons
- **App Management**: Launch, terminate, install, uninstall, list, and get foreground apps
- **App Management**: Launch, terminate, install, uninstall, clear data, list, and get foreground apps
- **Crash Reports**: List and fetch crash reports from iOS and Android devices

### 🎯 Platform Support
Expand Down Expand Up @@ -207,6 +207,10 @@ mobilecli apps install <path> --device <device-id>

# Uninstall an app
mobilecli apps uninstall <bundle-id> --device <device-id>

# Clear app data (cache, preferences, databases) without uninstalling
# Supported on Android and iOS Simulator
mobilecli apps clear <bundle-id> --device <device-id>
```

Example output for `apps foreground`:
Expand Down
22 changes: 22 additions & 0 deletions cli/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,26 @@ var appsUninstallCmd = &cobra.Command{
},
}

var appsClearCmd = &cobra.Command{
Use: "clear [bundle_id]",
Short: "Clear app data on a device",
Long: `Clears all data (cache, preferences, databases) for an app without uninstalling it. Supported on Android and iOS Simulator.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
req := commands.ClearAppRequest{
DeviceID: deviceId,
BundleID: args[0],
}

response := commands.ClearAppCommand(req)
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var appsForegroundCmd = &cobra.Command{
Use: "foreground",
Short: "Get the currently foreground app on a device",
Expand All @@ -158,6 +178,7 @@ func init() {
appsCmd.AddCommand(appsListCmd)
appsCmd.AddCommand(appsInstallCmd)
appsCmd.AddCommand(appsUninstallCmd)
appsCmd.AddCommand(appsClearCmd)
appsCmd.AddCommand(appsForegroundCmd)

appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on")
Expand All @@ -169,5 +190,6 @@ func init() {
appsInstallCmd.Flags().StringVar(&provisioningProfile, "provisioning-profile", "", "Path to a .mobileprovision file to use for re-signing")
appsInstallCmd.Flags().StringVar(&signingIdentity, "signing-identity", "", "Signing identity name to use for re-signing")
appsUninstallCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to uninstall app from")
appsClearCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to clear app data on")
appsForegroundCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get foreground app from")
}
25 changes: 25 additions & 0 deletions commands/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,31 @@ func InstallAppCommand(req InstallAppRequest) *CommandResponse {
})
}

type ClearAppRequest struct {
DeviceID string `json:"deviceId"`
BundleID string `json:"bundleId"`
}

func ClearAppCommand(req ClearAppRequest) *CommandResponse {
if req.BundleID == "" {
return NewErrorResponse(fmt.Errorf("bundle ID is required"))
}

targetDevice, err := FindDeviceOrAutoSelect(req.DeviceID)
if err != nil {
return NewErrorResponse(fmt.Errorf("error finding device: %w", err))
}

err = targetDevice.ClearApp(req.BundleID)
if err != nil {
return NewErrorResponse(fmt.Errorf("failed to clear app on device %s: %w", targetDevice.ID(), err))
}

return NewSuccessResponse(map[string]any{
"message": fmt.Sprintf("Cleared app '%s' on device %s", req.BundleID, targetDevice.ID()),
})
}

type UninstallAppRequest struct {
DeviceID string `json:"deviceId"`
PackageName string `json:"packageName"`
Expand Down
11 changes: 11 additions & 0 deletions devices/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,17 @@ func (d *AndroidDevice) InstallApp(path string) error {
return fmt.Errorf("installation failed: %s", string(output))
}

func (d *AndroidDevice) ClearApp(bundleID string) error {
output, err := d.runAdbCommand("shell", "pm", "clear", bundleID)
if err != nil {
return fmt.Errorf("failed to clear app %s: %w\nOutput: %s", bundleID, err, string(output))
}
if !strings.Contains(string(output), "Success") {
return fmt.Errorf("failed to clear app %s: %s", bundleID, strings.TrimSpace(string(output)))
}
return nil
}

func (d *AndroidDevice) UninstallApp(packageName string) (*InstalledAppInfo, error) {
appInfo := &InstalledAppInfo{
PackageName: packageName,
Expand Down
1 change: 1 addition & 0 deletions devices/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ type ControllableDevice interface {
GetForegroundApp() (*ForegroundAppInfo, error)
InstallApp(path string) error
UninstallApp(packageName string) (*InstalledAppInfo, error)
ClearApp(bundleID string) error
Info() (*FullDeviceInfo, error)
StartScreenCapture(config ScreenCaptureConfig) error
DumpSource() ([]ScreenElement, error)
Expand Down
4 changes: 4 additions & 0 deletions devices/ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,10 @@ func (d IOSDevice) InstallApp(path string) error {
return nil
}

func (d IOSDevice) ClearApp(bundleID string) error {
return fmt.Errorf("clearing app data is not supported on real iOS devices")
}

func (d IOSDevice) UninstallApp(packageName string) (*InstalledAppInfo, error) {
log.SetLevel(log.WarnLevel)

Expand Down
4 changes: 4 additions & 0 deletions devices/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,10 @@ func (r *RemoteDevice) UninstallApp(packageName string) (*InstalledAppInfo, erro
return nil, fmt.Errorf("uninstall app is not supported on remote devices")
}

func (r *RemoteDevice) ClearApp(bundleID string) error {
return r.fireRPC("device.apps.clear", params{"bundleId": bundleID})
}

// ScreenRecordCallbacks provides optional progress callbacks for screen recording
type ScreenRecordCallbacks struct {
OnRecordingEnded func()
Expand Down
27 changes: 27 additions & 0 deletions devices/simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,33 @@ func (s SimulatorDevice) InstallApp(path string) error {
return InstallApp(s.UDID, path)
}

func (s SimulatorDevice) ClearApp(bundleID string) error {
output, err := runSimctl("get_app_container", s.UDID, bundleID, "data")
if err != nil {
return fmt.Errorf("failed to get data container for %s: %w", bundleID, err)
}

containerPath := strings.TrimSpace(string(output))
if containerPath == "" {
return fmt.Errorf("no data container found for %s", bundleID)
}

_ = s.TerminateApp(bundleID)

entries, err := os.ReadDir(containerPath)
if err != nil {
return fmt.Errorf("failed to read data container: %w", err)
}

for _, entry := range entries {
if err := os.RemoveAll(filepath.Join(containerPath, entry.Name())); err != nil {
return fmt.Errorf("failed to remove %s: %w", entry.Name(), err)
}
}
Comment on lines +839 to +855
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate container path before recursive deletion.

Lines 839-855 perform recursive deletion based on get_app_container output without constraining the target path. Add a strict prefix/sanity check before os.RemoveAll to prevent accidental deletion outside the simulator app-data container.

🛡️ Suggested hardening patch
 func (s SimulatorDevice) ClearApp(bundleID string) error {
 	output, err := runSimctl("get_app_container", s.UDID, bundleID, "data")
 	if err != nil {
 		return fmt.Errorf("failed to get data container for %s: %w", bundleID, err)
 	}
 
-	containerPath := strings.TrimSpace(string(output))
+	containerPath := filepath.Clean(strings.TrimSpace(string(output)))
 	if containerPath == "" {
 		return fmt.Errorf("no data container found for %s", bundleID)
 	}
+	expectedRoot := filepath.Join(
+		os.Getenv("HOME"),
+		"Library", "Developer", "CoreSimulator", "Devices", s.UDID, "data", "Containers", "Data", "Application",
+	)
+	if containerPath == "." || containerPath == "/" ||
+		!strings.HasPrefix(containerPath+string(os.PathSeparator), expectedRoot+string(os.PathSeparator)) {
+		return fmt.Errorf("refusing to clear unexpected container path: %s", containerPath)
+	}
 
 	_ = s.TerminateApp(bundleID)
 
 	entries, err := os.ReadDir(containerPath)
 	if err != nil {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
containerPath := strings.TrimSpace(string(output))
if containerPath == "" {
return fmt.Errorf("no data container found for %s", bundleID)
}
_ = s.TerminateApp(bundleID)
entries, err := os.ReadDir(containerPath)
if err != nil {
return fmt.Errorf("failed to read data container: %w", err)
}
for _, entry := range entries {
if err := os.RemoveAll(filepath.Join(containerPath, entry.Name())); err != nil {
return fmt.Errorf("failed to remove %s: %w", entry.Name(), err)
}
}
containerPath := filepath.Clean(strings.TrimSpace(string(output)))
if containerPath == "" {
return fmt.Errorf("no data container found for %s", bundleID)
}
expectedRoot := filepath.Join(
os.Getenv("HOME"),
"Library", "Developer", "CoreSimulator", "Devices", s.UDID, "data", "Containers", "Data", "Application",
)
if containerPath == "." || containerPath == "/" ||
!strings.HasPrefix(containerPath+string(os.PathSeparator), expectedRoot+string(os.PathSeparator)) {
return fmt.Errorf("refusing to clear unexpected container path: %s", containerPath)
}
_ = s.TerminateApp(bundleID)
entries, err := os.ReadDir(containerPath)
if err != nil {
return fmt.Errorf("failed to read data container: %w", err)
}
for _, entry := range entries {
if err := os.RemoveAll(filepath.Join(containerPath, entry.Name())); err != nil {
return fmt.Errorf("failed to remove %s: %w", entry.Name(), err)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devices/simulator.go` around lines 839 - 855, The code unconditionally
deletes everything under containerPath (from get_app_container output) which is
dangerous; before calling os.RemoveAll in the loop, resolve and sanitize the
path (use filepath.Abs and filepath.Clean on containerPath and each joined path)
and verify it is strictly inside the expected simulator app-data root (e.g.,
compare that containerPath has the expected container root prefix with a
trailing separator or use filepath.Rel to ensure the relative path does not
start with ".."), also reject obvious unsafe values (empty string or root "/");
only after these checks proceed with s.TerminateApp and the RemoveAll calls
(references: containerPath, s.TerminateApp, filepath.Join, os.RemoveAll).


return nil
}

func (s SimulatorDevice) UninstallApp(packageName string) (*InstalledAppInfo, error) {
installedApps, err := s.ListInstalledApps()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions server/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func GetMethodRegistry() map[string]HandlerFunc {
"device.apps.foreground": handleAppsForeground,
"device.apps.install": handleAppsInstall,
"device.apps.uninstall": handleAppsUninstall,
"device.apps.clear": handleAppsClear,
"device.screenrecord": handleScreenRecord,
"device.screenrecord.stop": handleScreenRecordStop,
"device.crashes.list": handleCrashesList,
Expand Down
32 changes: 32 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,11 @@ type AppsUninstallParams struct {
BundleID string `json:"bundleId"`
}

type AppsClearParams struct {
DeviceID string `json:"deviceId"`
BundleID string `json:"bundleId"`
}

type ScreenRecordParams struct {
DeviceID string `json:"deviceId"`
Output string `json:"output"`
Expand Down Expand Up @@ -989,6 +994,33 @@ func handleAppsInstall(params json.RawMessage) (any, error) {
return response.Data, nil
}

func handleAppsClear(params json.RawMessage) (any, error) {
if len(params) == 0 {
return nil, fmt.Errorf("'params' is required with fields: deviceId, bundleId")
}

var p AppsClearParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid parameters: %w. Expected fields: deviceId, bundleId", err)
}

if p.BundleID == "" {
return nil, fmt.Errorf("'bundleId' is required")
}

req := commands.ClearAppRequest{
DeviceID: p.DeviceID,
BundleID: p.BundleID,
}

response := commands.ClearAppCommand(req)
if response.Status == "error" {
return nil, fmt.Errorf("%s", response.Error)
}

return response.Data, nil
}

func handleAppsUninstall(params json.RawMessage) (any, error) {
if len(params) == 0 {
return nil, fmt.Errorf("'params' is required with fields: deviceId, bundleId")
Expand Down
Loading