diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivity.java index 23c464bc02..e1d46f79b2 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivity.java @@ -23,6 +23,7 @@ import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import org.slf4j.Logger; @@ -33,6 +34,7 @@ import com.amaze.filemanager.filesystem.root.CopyFilesCommand; import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity; import com.amaze.filemanager.ui.fragments.DbViewerFragment; +import com.amaze.filemanager.utils.Utils; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -73,9 +75,9 @@ public void onCreate(Bundle savedInstanceState) { boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK); getSupportActionBar().setDisplayHomeAsUpEnabled(!useNewStack); - path = getIntent().getStringExtra("path"); + path = Utils.sanitizeInput(getIntent().getStringExtra("path")); - if (path == null) { + if (!isValidPath(path)) { Toast.makeText(this, R.string.operation_not_supported, Toast.LENGTH_SHORT).show(); finish(); return; @@ -163,7 +165,7 @@ protected void onDestroy() { @Override public boolean onPrepareOptionsMenu(Menu menu) { - toolbar.setTitle(pathFile.getName()); + setToolbarTitle(); return super.onPrepareOptionsMenu(menu); } @@ -171,7 +173,7 @@ public boolean onPrepareOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { onBackPressed(); - toolbar.setTitle(pathFile.getName()); + setToolbarTitle(); } return super.onOptionsItemSelected(item); } @@ -179,6 +181,35 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onBackPressed() { super.onBackPressed(); - toolbar.setTitle(pathFile.getName()); + setToolbarTitle(); + } + + private void setToolbarTitle() { + if (pathFile != null) { + toolbar.setTitle(pathFile.getName()); + } + } + + private boolean isValidPath(String path) { + // Check if the path is not null, not empty, and does not contain special characters + if (path == null || path.isEmpty()) { + return false; + } + + try { + File file = new File(path); + String canonicalPath = file.getCanonicalPath(); + // Prevent path traversal attacks + if (canonicalPath.contains("..")) { + return false; + } + // Check for valid database file extensions + if (!path.endsWith(".db") && !path.endsWith(".sqlite") && !path.endsWith(".sqlite3")) { + return false; + } + return file.exists() && file.isFile(); + } catch (IOException e) { + return false; + } } } diff --git a/app/src/main/java/com/amaze/filemanager/utils/Utils.java b/app/src/main/java/com/amaze/filemanager/utils/Utils.java index 9d2f2419a0..437360edef 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/Utils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/Utils.java @@ -230,6 +230,10 @@ public static boolean isDeviceInLandScape(Activity activity) { /** Sanitizes input from external application to avoid any attempt of command injection */ public static String sanitizeInput(String input) { + if (input == null || input.isEmpty()) { + return input; + } + // iterate through input and keep sanitizing until it's fully injection proof String sanitizedInput; String sanitizedInputTemp = input; diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivityTest.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivityTest.kt new file mode 100644 index 0000000000..55a7909931 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivityTest.kt @@ -0,0 +1,88 @@ +package com.amaze.filemanager.ui.activities + +import android.content.Intent +import android.os.Build +import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.P +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.shadows.ShadowMultiDex +import io.mockk.every +import io.mockk.spyk +import org.awaitility.Awaitility.await +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStorageManager +import org.robolectric.util.ReflectionHelpers +import java.io.File +import java.util.concurrent.TimeUnit + +/** + * Tests for [DatabaseViewerActivity]. + */ +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + shadows = [ShadowMultiDex::class, ShadowStorageManager::class], +) +class DatabaseViewerActivityTest { + /** + * Tests that the activity sanitizes the path in the intent to prevent malicious payloads + * from being executed. + */ + @Test + fun testSanitizePathInIntent() { + val maliciousPayload = "/sdcard/fake.db; uname -a > /sdcard/system_info.txt; echo" + + val intent = Intent().putExtra("path", maliciousPayload) + + val activity = + Robolectric.buildActivity( + DatabaseViewerActivity::class.java, + intent, + ) + .create().start().visible().get() + + await().atMost(10, TimeUnit.SECONDS).until { + activity.isFinishing && + ReflectionHelpers.getField(activity, "pathFile") == null + } + } + + /** + * Tests that the activity does not crash when the path in the intent is valid. + */ + @Test + fun testValidPathInIntent() { + // Create a temporary valid database file + val validDbFile = File.createTempFile("test", ".db") + validDbFile.deleteOnExit() + + val intent = Intent().putExtra("path", validDbFile.absolutePath) + + val activityController = + Robolectric.buildActivity( + DatabaseViewerActivity::class.java, + intent, + ) + .create() + + val activity = activityController.get() + val spyActivity = spyk(activity, recordPrivateCalls = true) + + // Mock the load method to do nothing + every { spyActivity invoke ("load") withArguments listOf(any()) } returns Unit + + activityController.start().visible() + + await().atMost(5, TimeUnit.SECONDS).until { + !spyActivity.isFinishing && + ReflectionHelpers + .getField(spyActivity, "pathFile")?.absolutePath == validDbFile.absolutePath + } + + // Clean up + validDbFile.delete() + } +}