Skip to content

Commit 8f80d21

Browse files
authored
Merge pull request #4440 from TeamAmaze/bugfix/database-viewer-fix-intent
Patch DatabaseViewerActivity.onCreate to ensure only valid file paths are processed
2 parents c2ebfd1 + 27de7c6 commit 8f80d21

File tree

3 files changed

+128
-5
lines changed

3 files changed

+128
-5
lines changed

app/src/main/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivity.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK;
2424

2525
import java.io.File;
26+
import java.io.IOException;
2627
import java.util.ArrayList;
2728

2829
import org.slf4j.Logger;
@@ -33,6 +34,7 @@
3334
import com.amaze.filemanager.filesystem.root.CopyFilesCommand;
3435
import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity;
3536
import com.amaze.filemanager.ui.fragments.DbViewerFragment;
37+
import com.amaze.filemanager.utils.Utils;
3638

3739
import android.database.Cursor;
3840
import android.database.sqlite.SQLiteDatabase;
@@ -73,9 +75,9 @@ public void onCreate(Bundle savedInstanceState) {
7375
boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK);
7476
getSupportActionBar().setDisplayHomeAsUpEnabled(!useNewStack);
7577

76-
path = getIntent().getStringExtra("path");
78+
path = Utils.sanitizeInput(getIntent().getStringExtra("path"));
7779

78-
if (path == null) {
80+
if (!isValidPath(path)) {
7981
Toast.makeText(this, R.string.operation_not_supported, Toast.LENGTH_SHORT).show();
8082
finish();
8183
return;
@@ -163,22 +165,51 @@ protected void onDestroy() {
163165

164166
@Override
165167
public boolean onPrepareOptionsMenu(Menu menu) {
166-
toolbar.setTitle(pathFile.getName());
168+
setToolbarTitle();
167169
return super.onPrepareOptionsMenu(menu);
168170
}
169171

170172
@Override
171173
public boolean onOptionsItemSelected(MenuItem item) {
172174
if (item.getItemId() == android.R.id.home) {
173175
onBackPressed();
174-
toolbar.setTitle(pathFile.getName());
176+
setToolbarTitle();
175177
}
176178
return super.onOptionsItemSelected(item);
177179
}
178180

179181
@Override
180182
public void onBackPressed() {
181183
super.onBackPressed();
182-
toolbar.setTitle(pathFile.getName());
184+
setToolbarTitle();
185+
}
186+
187+
private void setToolbarTitle() {
188+
if (pathFile != null) {
189+
toolbar.setTitle(pathFile.getName());
190+
}
191+
}
192+
193+
private boolean isValidPath(String path) {
194+
// Check if the path is not null, not empty, and does not contain special characters
195+
if (path == null || path.isEmpty()) {
196+
return false;
197+
}
198+
199+
try {
200+
File file = new File(path);
201+
String canonicalPath = file.getCanonicalPath();
202+
// Prevent path traversal attacks
203+
if (canonicalPath.contains("..")) {
204+
return false;
205+
}
206+
// Check for valid database file extensions
207+
if (!path.endsWith(".db") && !path.endsWith(".sqlite") && !path.endsWith(".sqlite3")) {
208+
return false;
209+
}
210+
return file.exists() && file.isFile();
211+
} catch (IOException e) {
212+
return false;
213+
}
183214
}
184215
}

app/src/main/java/com/amaze/filemanager/utils/Utils.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ public static boolean isDeviceInLandScape(Activity activity) {
230230

231231
/** Sanitizes input from external application to avoid any attempt of command injection */
232232
public static String sanitizeInput(String input) {
233+
if (input == null || input.isEmpty()) {
234+
return input;
235+
}
236+
233237
// iterate through input and keep sanitizing until it's fully injection proof
234238
String sanitizedInput;
235239
String sanitizedInputTemp = input;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.amaze.filemanager.ui.activities
2+
3+
import android.content.Intent
4+
import android.os.Build
5+
import android.os.Build.VERSION_CODES.LOLLIPOP
6+
import android.os.Build.VERSION_CODES.P
7+
import androidx.test.ext.junit.runners.AndroidJUnit4
8+
import com.amaze.filemanager.shadows.ShadowMultiDex
9+
import io.mockk.every
10+
import io.mockk.spyk
11+
import org.awaitility.Awaitility.await
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.robolectric.Robolectric
15+
import org.robolectric.annotation.Config
16+
import org.robolectric.shadows.ShadowStorageManager
17+
import org.robolectric.util.ReflectionHelpers
18+
import java.io.File
19+
import java.util.concurrent.TimeUnit
20+
21+
/**
22+
* Tests for [DatabaseViewerActivity].
23+
*/
24+
@RunWith(AndroidJUnit4::class)
25+
@Config(
26+
sdk = [LOLLIPOP, P, Build.VERSION_CODES.R],
27+
shadows = [ShadowMultiDex::class, ShadowStorageManager::class],
28+
)
29+
class DatabaseViewerActivityTest {
30+
/**
31+
* Tests that the activity sanitizes the path in the intent to prevent malicious payloads
32+
* from being executed.
33+
*/
34+
@Test
35+
fun testSanitizePathInIntent() {
36+
val maliciousPayload = "/sdcard/fake.db; uname -a > /sdcard/system_info.txt; echo"
37+
38+
val intent = Intent().putExtra("path", maliciousPayload)
39+
40+
val activity =
41+
Robolectric.buildActivity(
42+
DatabaseViewerActivity::class.java,
43+
intent,
44+
)
45+
.create().start().visible().get()
46+
47+
await().atMost(10, TimeUnit.SECONDS).until {
48+
activity.isFinishing &&
49+
ReflectionHelpers.getField<File>(activity, "pathFile") == null
50+
}
51+
}
52+
53+
/**
54+
* Tests that the activity does not crash when the path in the intent is valid.
55+
*/
56+
@Test
57+
fun testValidPathInIntent() {
58+
// Create a temporary valid database file
59+
val validDbFile = File.createTempFile("test", ".db")
60+
validDbFile.deleteOnExit()
61+
62+
val intent = Intent().putExtra("path", validDbFile.absolutePath)
63+
64+
val activityController =
65+
Robolectric.buildActivity(
66+
DatabaseViewerActivity::class.java,
67+
intent,
68+
)
69+
.create()
70+
71+
val activity = activityController.get()
72+
val spyActivity = spyk(activity, recordPrivateCalls = true)
73+
74+
// Mock the load method to do nothing
75+
every { spyActivity invoke ("load") withArguments listOf(any<File>()) } returns Unit
76+
77+
activityController.start().visible()
78+
79+
await().atMost(5, TimeUnit.SECONDS).until {
80+
!spyActivity.isFinishing &&
81+
ReflectionHelpers
82+
.getField<File>(spyActivity, "pathFile")?.absolutePath == validDbFile.absolutePath
83+
}
84+
85+
// Clean up
86+
validDbFile.delete()
87+
}
88+
}

0 commit comments

Comments
 (0)