Skip to content

Commit 58481ae

Browse files
committed
Patch DatabaseViewerActivity.onCreate to ensure only valid file paths are processed
1 parent 2a857ac commit 58481ae

File tree

3 files changed

+129
-5
lines changed

3 files changed

+129
-5
lines changed

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

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

2525
import java.io.File;
26+
import java.io.FileNotFoundException;
27+
import java.io.IOException;
2628
import java.util.ArrayList;
2729

2830
import org.slf4j.Logger;
@@ -33,6 +35,7 @@
3335
import com.amaze.filemanager.filesystem.root.CopyFilesCommand;
3436
import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity;
3537
import com.amaze.filemanager.ui.fragments.DbViewerFragment;
38+
import com.amaze.filemanager.utils.Utils;
3639

3740
import android.database.Cursor;
3841
import android.database.sqlite.SQLiteDatabase;
@@ -73,9 +76,9 @@ public void onCreate(Bundle savedInstanceState) {
7376
boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK);
7477
getSupportActionBar().setDisplayHomeAsUpEnabled(!useNewStack);
7578

76-
path = getIntent().getStringExtra("path");
79+
path = Utils.sanitizeInput(getIntent().getStringExtra("path"));
7780

78-
if (path == null) {
81+
if (!isValidPath(path)) {
7982
Toast.makeText(this, R.string.operation_not_supported, Toast.LENGTH_SHORT).show();
8083
finish();
8184
return;
@@ -163,22 +166,51 @@ protected void onDestroy() {
163166

164167
@Override
165168
public boolean onPrepareOptionsMenu(Menu menu) {
166-
toolbar.setTitle(pathFile.getName());
169+
setToolbarTitle();
167170
return super.onPrepareOptionsMenu(menu);
168171
}
169172

170173
@Override
171174
public boolean onOptionsItemSelected(MenuItem item) {
172175
if (item.getItemId() == android.R.id.home) {
173176
onBackPressed();
174-
toolbar.setTitle(pathFile.getName());
177+
setToolbarTitle();
175178
}
176179
return super.onOptionsItemSelected(item);
177180
}
178181

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

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)