Conversation
|
/backport to stable-3.36 |
15f6f41 to
d6354a5
Compare
d6354a5 to
62e0195
Compare
|
|
||
| Log_OC.d(TAG, "removeLocalCopyIfNeeded: deleting local file -> " + localPath); | ||
|
|
||
| boolean success = new File(localPath).delete(); |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix uncontrolled path usage you either validate the incoming path against a safe base directory or ensure that only application-generated paths (not arbitrary user input) are used. Here, we can keep the existing behavior but add a safety check before deleting a file based on OCFile.getStoragePath(). The key idea is: compute the canonical path of the candidate file and of the application’s configured storage root, and only proceed with deletion if the file path is within (a descendant of) that root. This directly mitigates the risk of a malicious or corrupted storagePath pointing outside the intended storage area.
The best minimal fix, without changing higher-level behavior, is in FileDataStorageManager.removeLocalCopyIfNeeded. Right before constructing new File(localPath) and deleting it, we should:
- Obtain the current configured storage root from
MainApp.getStoragePath(). - If that storage root is empty/null, we bail out (no deletion).
- Resolve both the storage root and the target file to canonical paths (
getCanonicalFile()). - Check
fileCanonical.getPath().startsWith(rootCanonical.getPath() + File.separator). - If the check fails, log a warning and skip deletion.
This ensures that even if localPath has been tainted (for example, via a compromised database entry or weird storage configuration), we will not delete anything outside the app’s own storage tree. We do not need extra imports since java.io.File is already in use. All changes are confined to the existing method body in FileDataStorageManager and do not alter public APIs or other behavior except for refusing to delete files outside the configured storage root.
| @@ -930,9 +930,35 @@ | ||
| return true; | ||
| } | ||
|
|
||
| // Ensure that the local path is within the configured storage root before deleting | ||
| String storageRoot = MainApp.getStoragePath(); | ||
| if (TextUtils.isEmpty(storageRoot)) { | ||
| Log_OC.e(TAG, "removeLocalCopyIfNeeded: storage root is not configured, skip deletion for " + localPath); | ||
| return false; | ||
| } | ||
|
|
||
| File targetFile = new File(localPath); | ||
| File rootDir = new File(storageRoot); | ||
|
|
||
| try { | ||
| File targetCanonical = targetFile.getCanonicalFile(); | ||
| File rootCanonical = rootDir.getCanonicalFile(); | ||
|
|
||
| String rootPath = rootCanonical.getPath(); | ||
| String targetPath = targetCanonical.getPath(); | ||
|
|
||
| if (!targetPath.startsWith(rootPath + File.separator)) { | ||
| Log_OC.e(TAG, "removeLocalCopyIfNeeded: refusing to delete file outside storage root: " + targetPath); | ||
| return false; | ||
| } | ||
| } catch (IOException e) { | ||
| Log_OC.e(TAG, "removeLocalCopyIfNeeded: error resolving canonical paths for " + localPath, e); | ||
| return false; | ||
| } | ||
|
|
||
| Log_OC.d(TAG, "removeLocalCopyIfNeeded: deleting local file -> " + localPath); | ||
|
|
||
| boolean success = new File(localPath).delete(); | ||
| boolean success = targetFile.delete(); | ||
| Log_OC.d(TAG, "removeLocalCopyIfNeeded: file deletion result=" + success); | ||
|
|
||
| if (!success) { |
| ocFile.setStoragePath(null); | ||
| saveFile(ocFile); | ||
| } | ||
| if (!localFolder.exists()) { |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, the problem should be fixed by validating and constraining any user-controllable storage path before it is persisted or used to construct file operation paths. For a “root storage location” setting, the safe approach is to ensure the chosen directory is within a set of allowed base directories (e.g., the app’s internal files dir, app-specific external storage, or other app-managed roots), and that the path is normalized (no .. traversal, no control characters). Storing only validated paths in MainApp.storagePath ensures all downstream uses (like FileStorageUtils.getSavePath and getDefaultSavePathFor) are safe.
The best fix with minimal functional change is:
-
Introduce a reusable validator in
FileStorageUtilsthat:- Rejects null/empty inputs.
- Normalizes the candidate path (
canonicalPath). - Ensures it is inside one of the allowed roots for the app (for example, the app’s internal files directory and/or an app-specific external files directory).
- Optionally enforces a maximum length and disallows control characters.
The method should return a boolean indicating whether the path is acceptable.
-
Use this validator at the point where the path first becomes persistent application state:
- In
SettingsActivity.saveStoragePath(String newStoragePath), checkFileStorageUtils.isValidStorageRoot(getApplicationContext(), newStoragePath)(or similar). - If invalid, do not persist it and show an error to the user (or silently fall back to the previous/default storage path). This prevents
MainApp.setStoragePathfrom ever seeing an unsafe value.
- In
-
Optionally, add a defensive check in
MainApp.setStoragePath(String path)to avoid storing obviously invalid values (e.g., null/empty). However, the core security fix is at the UI/settings layer. -
No changes are required in
FileDataStorageManager.removeLocalFolderitself, because onceMainApp.storagePathis guaranteed to be valid,FileStorageUtils.getDefaultSavePathForand the paths it builds become safe by construction.
Concretely:
-
Edit
FileStorageUtils.javato add:- An import for
android.os.Environmentis already present; we can also safely useContextwhich is already imported. - A new public static method
isValidStorageRoot(Context context, String path)that performs normalization and containment checks usingnew File(path).getCanonicalPath()and compares it to allowed base directories obtained fromcontext.getFilesDir()andcontext.getExternalFilesDir(null)(if non-null).
- An import for
-
Edit
SettingsActivity.java:- In
saveStoragePath(String newStoragePath), before assigning tostoragePathand callingMainApp.setStoragePath, callFileStorageUtils.isValidStorageRoot(getApplicationContext(), newStoragePath). - If validation fails, log and either keep the existing
storagePathor fall back to the default (getFilesDir().getAbsolutePath()); do not save the unvalidated path.
- In
These changes keep existing behavior for normal, valid inputs but prevent arbitrary or malicious paths from being used for storage or deletion.
| @@ -1188,7 +1188,18 @@ | ||
| */ | ||
| private void saveStoragePath(String newStoragePath) { | ||
| SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); | ||
| storagePath = newStoragePath; | ||
|
|
||
| // Validate the new storage path to avoid using arbitrary file system locations. | ||
| if (!FileStorageUtils.isValidStorageRoot(getApplicationContext(), newStoragePath)) { | ||
| Log_OC.e(TAG, "Refusing to save invalid storage path: " + newStoragePath); | ||
| // Keep existing storagePath; if none is set yet, ensure a safe default. | ||
| if (storagePath == null) { | ||
| storagePath = getApplicationContext().getFilesDir().getAbsolutePath(); | ||
| } | ||
| } else { | ||
| storagePath = newStoragePath; | ||
| } | ||
|
|
||
| MainApp.setStoragePath(storagePath); | ||
| SharedPreferences.Editor editor = appPrefs.edit(); | ||
| editor.putString(AppPreferencesImpl.STORAGE_PATH, storagePath); |
| @@ -157,6 +157,48 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Validate that a candidate storage root path is inside an app-controlled directory. | ||
| * | ||
| * This prevents arbitrary paths from being used as storage roots, which could otherwise | ||
| * lead to unintended access or deletion of files outside the app's sandbox. | ||
| * | ||
| * @param context application context used to resolve allowed base directories | ||
| * @param path candidate storage root path | ||
| * @return true if the path is considered safe to use as a storage root, false otherwise | ||
| */ | ||
| public static boolean isValidStorageRoot(Context context, String path) { | ||
| if (context == null || TextUtils.isEmpty(path)) { | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| File candidate = new File(path); | ||
| String candidateCanonical = candidate.getCanonicalPath(); | ||
|
|
||
| // Internal app files directory (always present) | ||
| File internalDir = context.getFilesDir(); | ||
| String internalCanonical = internalDir != null ? internalDir.getCanonicalPath() : null; | ||
|
|
||
| // App-specific external files directory (may be null) | ||
| File externalDir = context.getExternalFilesDir(null); | ||
| String externalCanonical = externalDir != null ? externalDir.getCanonicalPath() : null; | ||
|
|
||
| // Require the candidate to be within one of the allowed roots | ||
| if (internalCanonical != null && candidateCanonical.startsWith(internalCanonical)) { | ||
| return true; | ||
| } | ||
| if (externalCanonical != null && candidateCanonical.startsWith(externalCanonical)) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } catch (IOException e) { | ||
| Log_OC.e("FileStorageUtils", "Error validating storage root path: " + path, e); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get local owncloud storage path for accountName. | ||
| */ | ||
| public static String getSavePath(String accountName) { |
| File localFile = new File(ocFile.getStoragePath()); | ||
| Log_OC.d(TAG, "removeLocalFolder: deleting file -> " + ocFile.getStoragePath()); | ||
|
|
||
| boolean deleted = localFile.delete(); |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix “uncontrolled data used in path expression” issues, we must ensure that any path derived from user-controllable input is either (a) validated/normalized against an allowlist of safe locations or (b) constrained to a specific directory tree, rejecting anything that escapes that tree. For this code path, the user controls the storage root path through settings; that root then influences where per-account files are stored and what storagePath the OCFile model holds. Deletion in removeLocalFolder must not operate on files outside the current storage root.
The best minimal fix without changing behaviour is to: (1) validate and normalize the storage root when it is saved (in SettingsActivity.saveStoragePath), and (2) guard removeLocalFolder so that it only deletes files whose storagePath is under the configured storage root. We will add a static helper to FileStorageUtils that encapsulates “safe storage root validation” and reuse it in both places.
Concrete changes:
-
Add validation helper in
FileStorageUtils- Implement a public static method, e.g.
ensureValidStoragePath(String requestedPath), that:- Returns the default internal storage path if
requestedPathis null/empty. - Attempts to canonicalize the requested path (
new File(requestedPath).getCanonicalFile()). - Canonicalizes the app’s internal files directory root (
getAppContext().getFilesDir()). - If the requested path is not under some acceptable base (here, at least not under the root path
/alone), we still accept it but ensure we store the canonical absolute path (to remove.., symlinks, etc.).
To satisfy the CodeQL rule more robustly and avoid directory traversal, we check that the canonical requested path is an absolute path and not equal to/and not containing..segments; otherwise we fall back to default internal storage.
- Returns the default internal storage path if
- This helper only uses standard
java.io.FileandMainApp.getAppContext(), which are already present, so no new dependencies are needed.
- Implement a public static method, e.g.
-
Use validation when saving storage path in
SettingsActivity- In
saveStoragePath(String newStoragePath), instead of directly assigningstoragePath = newStoragePath, we callFileStorageUtils.ensureValidStoragePath(newStoragePath)and store that safe/normalized result. This ensures thatMainApp.setStoragePathand subsequentFileStorageUtils.getSavePathcalls operate on a sanitized root path.
- In
-
Restrict deletions in
FileDataStorageManager.removeLocalFolder- Before deleting per-file paths, we ensure that the
ocFile.getStoragePath()is non-null and lies under the current storage root (MainApp.getStoragePath()), using canonical path comparison (startsWithon canonical paths). If the file is outside the storage root, we skip deletion and log a warning instead of deleting it. - This prevents accidental deletion of arbitrary files if a
storagePathin the DB is inconsistent with the configured storage root, and clearly constrains deletion to the intended data tree.
- Before deleting per-file paths, we ensure that the
These changes leave the functional behaviour unchanged for valid, normal configurations, but add guardrails and satisfy the requirement to validate user-influenced paths before using them for file operations.
| @@ -1023,18 +1023,36 @@ | ||
|
|
||
| } else if (ocFile.isDown()) { | ||
|
|
||
| File localFile = new File(ocFile.getStoragePath()); | ||
| Log_OC.d(TAG, "removeLocalFolder: deleting file -> " + ocFile.getStoragePath()); | ||
| String storagePath = ocFile.getStoragePath(); | ||
| if (TextUtils.isEmpty(storagePath)) { | ||
| Log_OC.w(TAG, "removeLocalFolder: skipping deletion, storage path is empty"); | ||
| continue; | ||
| } | ||
|
|
||
| boolean deleted = localFile.delete(); | ||
| success = deleted; | ||
| try { | ||
| File localFile = new File(storagePath); | ||
| File canonicalLocalFile = localFile.getCanonicalFile(); | ||
| File storageRoot = new File(MainApp.getStoragePath()).getCanonicalFile(); | ||
|
|
||
| Log_OC.d(TAG, "removeLocalFolder: file deletion result=" + deleted); | ||
| if (!canonicalLocalFile.getPath().startsWith(storageRoot.getPath() + File.separator)) { | ||
| Log_OC.w(TAG, "removeLocalFolder: refusing to delete file outside storage root: " + canonicalLocalFile); | ||
| continue; | ||
| } | ||
|
|
||
| if (deleted) { | ||
| deleteFileInMediaScan(ocFile.getStoragePath()); | ||
| ocFile.setStoragePath(null); | ||
| saveFile(ocFile); | ||
| Log_OC.d(TAG, "removeLocalFolder: deleting file -> " + canonicalLocalFile.getAbsolutePath()); | ||
| boolean deleted = canonicalLocalFile.delete(); | ||
| success = deleted; | ||
|
|
||
| Log_OC.d(TAG, "removeLocalFolder: file deletion result=" + deleted); | ||
|
|
||
| if (deleted) { | ||
| deleteFileInMediaScan(canonicalLocalFile.getAbsolutePath()); | ||
| ocFile.setStoragePath(null); | ||
| saveFile(ocFile); | ||
| } | ||
| } catch (IOException e) { | ||
| Log_OC.e(TAG, "removeLocalFolder: failed to resolve canonical path for " + storagePath, e); | ||
| success = false; | ||
| } | ||
| } | ||
| } |
| @@ -73,6 +73,7 @@ | ||
| import com.owncloud.android.utils.DeviceCredentialUtils; | ||
| import com.owncloud.android.utils.DisplayUtils; | ||
| import com.owncloud.android.utils.EncryptionUtils; | ||
| import com.owncloud.android.utils.FileStorageUtils; | ||
| import com.owncloud.android.utils.MimeTypeUtil; | ||
| import com.owncloud.android.utils.PermissionUtil; | ||
| import com.owncloud.android.utils.theme.CapabilityUtils; | ||
| @@ -1188,7 +1189,8 @@ | ||
| */ | ||
| private void saveStoragePath(String newStoragePath) { | ||
| SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); | ||
| storagePath = newStoragePath; | ||
| // Normalize and validate the user-selected storage path before persisting it. | ||
| storagePath = FileStorageUtils.ensureValidStoragePath(newStoragePath); | ||
| MainApp.setStoragePath(storagePath); | ||
| SharedPreferences.Editor editor = appPrefs.edit(); | ||
| editor.putString(AppPreferencesImpl.STORAGE_PATH, storagePath); |
| @@ -157,6 +157,44 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Normalize and validate a storage root path before persisting/using it. | ||
| * <p> | ||
| * If the requested path is null, empty, not absolute, or resolves to an | ||
| * unsafe location, this method falls back to the app's internal files dir. | ||
| * | ||
| * @param requestedPath path selected/configured by the user. | ||
| * @return a canonical, absolute, safe storage root path. | ||
| */ | ||
| public static String ensureValidStoragePath(@Nullable String requestedPath) { | ||
| File defaultRoot = MainApp.getAppContext().getFilesDir(); | ||
| try { | ||
| File defaultCanonical = defaultRoot.getCanonicalFile(); | ||
|
|
||
| if (TextUtils.isEmpty(requestedPath)) { | ||
| return defaultCanonical.getAbsolutePath(); | ||
| } | ||
|
|
||
| File requestedFile = new File(requestedPath); | ||
| if (!requestedFile.isAbsolute()) { | ||
| return defaultCanonical.getAbsolutePath(); | ||
| } | ||
|
|
||
| File requestedCanonical = requestedFile.getCanonicalFile(); | ||
|
|
||
| // Very defensive checks: disallow using filesystem root or parent traversal. | ||
| String requestedCanonicalPath = requestedCanonical.getPath(); | ||
| if ("/".equals(requestedCanonicalPath) || requestedCanonicalPath.contains("..")) { | ||
| return defaultCanonical.getAbsolutePath(); | ||
| } | ||
|
|
||
| return requestedCanonicalPath; | ||
| } catch (IOException e) { | ||
| Log_OC.e("FileStorageUtils", "Failed to validate storage path, using default", e); | ||
| return defaultRoot.getAbsolutePath(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get local owncloud storage path for accountName. | ||
| */ | ||
| public static String getSavePath(String accountName) { |
| return false; | ||
| } | ||
|
|
||
| if (!localFolder.exists()) { |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix uncontrolled path usage you must validate and constrain any user-provided path before it is stored or used. For a storage root, the usual pattern is: (1) normalize the candidate path, (2) ensure it resides under one of a small set of allowed base directories (e.g., app-internal files directory and possibly app-specific external storage), and (3) reject or ignore values that are empty, invalid, or point outside those bases.
For this concrete case, the most robust and least invasive fix is to validate the storage path when it is (a) received from the Intent result, and (b) persisted in preferences and assigned to MainApp.storagePath. That ensures all downstream uses (including FileStorageUtils.getSavePath and removeLocalFolder(File)) only ever see safe values. We can implement a dedicated validator method in SettingsActivity to check the new storage location against the app’s internal files directory (and possibly other allowed roots if desired). The validator will use java.io.File’s getCanonicalFile to normalize paths and then check that the candidate path starts with the allowed base. If validation fails, the migration will not be started, and we can optionally show a message or just ignore the change. Additionally, we should guard MainApp.setStoragePath against null or obviously invalid strings (e.g., empty) so that a bad value cannot be propagated globally, even if some call path accidentally bypassed saveStoragePath.
Concretely:
- In
SettingsActivity, add a private methodisValidStoragePath(String candidate)that:- Returns false on null/blank.
- Resolves the candidate to a canonical file.
- Resolves the app’s internal files directory to a canonical file.
- Checks
candidateCanonical.getPath().startsWith(internalCanonical.getPath()).
- In
onActivityResult, when handlingExtendedSettingsActivityDialog.StorageLocation, only constructStorageMigrationand callmigrate()ifisValidStoragePath(newPath)returns true. - In
saveStoragePath, validate thenewStoragePathbefore writing it to preferences and before callingMainApp.setStoragePath; if invalid, log and return without changing the stored path. - In
MainApp.setStoragePath, add a simple null/blank guard to prevent obviously bogus values, without changing any existing semantics for valid strings.
This keeps existing functionality for normal, UI-generated paths (which should already be under the app’s internal directory), while blocking crafted or malicious paths from being used as storage roots.
| @@ -757,7 +757,16 @@ | ||
| } | ||
|
|
||
| public static void setStoragePath(String path) { | ||
| MainApp.storagePath = path; | ||
| if (path == null) { | ||
| return; | ||
| } | ||
|
|
||
| String trimmed = path.trim(); | ||
| if (trimmed.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| MainApp.storagePath = trimmed; | ||
| } | ||
|
|
||
| // Methods to obtain Strings referring app_name |
| @@ -78,6 +78,8 @@ | ||
| import com.owncloud.android.utils.theme.CapabilityUtils; | ||
| import com.owncloud.android.utils.theme.ViewThemeUtils; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.util.Objects; | ||
|
|
||
| import javax.inject.Inject; | ||
| @@ -1022,7 +1024,7 @@ | ||
| startActivity(i); | ||
| } else if (requestCode == ExtendedSettingsActivityDialog.StorageLocation.getResultId() && data != null) { | ||
| String newPath = data.getStringExtra(ExtendedSettingsActivityDialog.StorageLocation.getKey()); | ||
| if (storagePath != null && !storagePath.equals(newPath)) { | ||
| if (storagePath != null && !storagePath.equals(newPath) && isValidStoragePath(newPath)) { | ||
| StorageMigration storageMigration = new StorageMigration(this, user, storagePath, newPath, viewThemeUtils); | ||
| storageMigration.setStorageMigrationProgressListener(this); | ||
| storageMigration.migrate(); | ||
| @@ -1187,6 +1189,11 @@ | ||
| * Save storage path | ||
| */ | ||
| private void saveStoragePath(String newStoragePath) { | ||
| if (!isValidStoragePath(newStoragePath)) { | ||
| Log_OC.w(TAG, "Ignoring invalid storage path: " + newStoragePath); | ||
| return; | ||
| } | ||
|
|
||
| SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); | ||
| storagePath = newStoragePath; | ||
| MainApp.setStoragePath(storagePath); | ||
| @@ -1195,6 +1202,35 @@ | ||
| editor.apply(); | ||
| } | ||
|
|
||
| /** | ||
| * Validate that the provided storage path is within the app's internal files directory. | ||
| */ | ||
| private boolean isValidStoragePath(String candidate) { | ||
| if (candidate == null) { | ||
| return false; | ||
| } | ||
|
|
||
| String trimmed = candidate.trim(); | ||
| if (trimmed.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| File baseDir = getApplicationContext().getFilesDir(); | ||
| File baseCanonical = baseDir.getCanonicalFile(); | ||
| File candidateCanonical = new File(trimmed).getCanonicalFile(); | ||
|
|
||
| String basePath = baseCanonical.getPath(); | ||
| String candidatePath = candidateCanonical.getPath(); | ||
|
|
||
| // Ensure the candidate is inside the app's internal files directory. | ||
| return candidatePath.startsWith(basePath + File.separator) || candidatePath.equals(basePath); | ||
| } catch (IOException e) { | ||
| Log_OC.e(TAG, "Failed to validate storage path: " + candidate, e); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private void readStoragePath() { | ||
| SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); | ||
| // Load storage path from shared preferences. Use private internal storage by default. |
| success &= localFolder.delete(); | ||
|
|
||
| return success; | ||
| boolean folderDeleted = localFolder.delete(); |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, the problem should be fixed by validating and constraining the storage base path before it is accepted and persisted, and by ensuring that any deletion routines only operate within a known-safe area. The safest approach is to restrict storage locations to a small set of app-controlled roots (for example, app-internal storage and possibly app-private external directories), and reject any path that falls outside these roots. At minimum, the path should be normalized and checked to be an absolute path under one of those roots, without any .. components or other tricks.
For this codebase, the least intrusive and most robust fix is:
-
Introduce a validation helper in
FileStorageUtils(which already hosts filesystem-related helpers) that takes a candidate storage path and returns either a normalized, validated version ornullif invalid. This helper can:- Reject empty/null strings.
- Normalize the path (using
new File(path).getCanonicalFile()). - Require it to be absolute.
- Require it to start with one of the allowed base directories: the app’s internal files directory (
context.getFilesDir()) and, if desired, itsgetExternalFilesDir(null). BecauseFileStorageUtilsis static, it can obtain aContextviaMainApp.getAppContext().
-
Use this helper in
SettingsActivity.saveStoragePathto validate any new storage path before persisting it and before callingMainApp.setStoragePath. If invalid, log and either:- fall back to the current stored path (no change), or
- fall back to the default internal storage path.
To avoid changing user-visible behavior too much, the best compromise is to keep the old path if the requested new one is invalid.
-
Ensure that
MainApp.storagePathis always non-null and points to something sensible. We can add a small guard inMainApp.getStoragePath()that falls back to the default internal storage directory ifstoragePathis null or empty, usingMainApp.getAppContext().
With these changes, any localFolder passed into removeLocalFolder(File) that is derived from MainApp.getStoragePath() will be anchored under validated directories only, eliminating the possibility of deleting arbitrary, attacker-chosen filesystem locations.
| @@ -745,7 +745,7 @@ | ||
| } | ||
|
|
||
| public static Context getAppContext() { | ||
| return MainApp.appContext.get(); | ||
| return MainApp.appContext != null ? MainApp.appContext.get() : null; | ||
| } | ||
|
|
||
| public static void setAppContext(Context context) { | ||
| @@ -753,7 +753,17 @@ | ||
| } | ||
|
|
||
| public static String getStoragePath() { | ||
| return MainApp.storagePath; | ||
| if (!TextUtils.isEmpty(MainApp.storagePath)) { | ||
| return MainApp.storagePath; | ||
| } | ||
|
|
||
| Context context = getAppContext(); | ||
| if (context != null) { | ||
| return context.getFilesDir().getAbsolutePath(); | ||
| } | ||
|
|
||
| // As a last resort, fall back to the external storage directory. | ||
| return Environment.getExternalStorageDirectory().getAbsolutePath(); | ||
| } | ||
|
|
||
| public static void setStoragePath(String path) { |
| @@ -1188,7 +1188,16 @@ | ||
| */ | ||
| private void saveStoragePath(String newStoragePath) { | ||
| SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); | ||
| storagePath = newStoragePath; | ||
|
|
||
| // Validate and normalize the requested storage path before persisting it. | ||
| String normalizedPath = FileStorageUtils.validateAndNormalizeStoragePath(newStoragePath); | ||
| if (normalizedPath == null) { | ||
| Log_OC.e(TAG, "Rejected invalid storage path: " + newStoragePath); | ||
| // Do not change the existing storagePath if the new one is invalid. | ||
| return; | ||
| } | ||
|
|
||
| storagePath = normalizedPath; | ||
| MainApp.setStoragePath(storagePath); | ||
| SharedPreferences.Editor editor = appPrefs.edit(); | ||
| editor.putString(AppPreferencesImpl.STORAGE_PATH, storagePath); |
| @@ -157,6 +157,63 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Validate and normalize a candidate base storage path. | ||
| * <p> | ||
| * The returned path, if non-null, is guaranteed to be an absolute, canonical | ||
| * directory that resides under one of the allowed application-specific roots | ||
| * (internal files directory or, if available, external files directory). | ||
| * | ||
| * @param rawPath the user-provided storage path | ||
| * @return a normalized, safe storage path, or {@code null} if the input is invalid | ||
| */ | ||
| @Nullable | ||
| public static String validateAndNormalizeStoragePath(@Nullable String rawPath) { | ||
| if (TextUtils.isEmpty(rawPath)) { | ||
| return null; | ||
| } | ||
|
|
||
| Context context = MainApp.getAppContext(); | ||
| if (context == null) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| File candidate = new File(rawPath).getCanonicalFile(); | ||
| if (!candidate.isAbsolute()) { | ||
| return null; | ||
| } | ||
|
|
||
| File internalRoot = context.getFilesDir().getCanonicalFile(); | ||
| File externalRoot = context.getExternalFilesDir(null); | ||
| if (externalRoot != null) { | ||
| externalRoot = externalRoot.getCanonicalFile(); | ||
| } | ||
|
|
||
| String candidatePath = candidate.getPath(); | ||
| String internalPath = internalRoot.getPath(); | ||
|
|
||
| boolean underInternal = candidatePath.startsWith(internalPath + File.separator) | ||
| || candidatePath.equals(internalPath); | ||
|
|
||
| boolean underExternal = false; | ||
| if (externalRoot != null) { | ||
| String externalPath = externalRoot.getPath(); | ||
| underExternal = candidatePath.startsWith(externalPath + File.separator) | ||
| || candidatePath.equals(externalPath); | ||
| } | ||
|
|
||
| if (underInternal || underExternal) { | ||
| return candidatePath; | ||
| } | ||
|
|
||
| return null; | ||
| } catch (IOException e) { | ||
| Log_OC.e(FileStorageUtils.class.getSimpleName(), "Failed to canonicalize storage path: " + rawPath, e); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get local owncloud storage path for accountName. | ||
| */ | ||
| public static String getSavePath(String accountName) { |
| } | ||
|
|
||
| @Test | ||
| fun deleteFolderRecursive() { |
There was a problem hiding this comment.
This "only" tests first folder hierarchy.
Please add a real hierarchy / recursive test.
Signed-off-by: alperozturk96 <alper_ozturk@proton.me>
62e0195 to
d47cb54
Compare
Signed-off-by: alperozturk96 <alper_ozturk@proton.me>
|
APK file: https://www.kaminsky.me/nc-dev/android-artifacts/16621.apk |
|
blue-Light-Screenshot test failed, but no output was generated. Maybe a preliminary stage failed. |


Issue
Child of directory is not gets deleted.
getContentProviderClient()is always null andgetContentResolver().delete(folderUri, where, whereArgs);not removing the childs just deletes folder itself see where logic.
Changes
Replaces content resolver functions with
DAOfunctions.Applies fail fast princible and adds logs to follow execution flow more clearly.
Adds tests.