Skip to main content
  1. posts/

Content & File Providers

·4742 words·23 mins·
Summary Andorid Contnet Provider File Provider
Table of Contents

Summary
#

A way to manage access to data and make sharing this data with other applications easier. Think of it like having notes saved in one app (A) that you want other apps (B,C,D etc…) to access - but they’re saved locally so Android will deny access for any other app. OK Simple, just call the provider and it will provide it for us! 😄

They are designed to manage access to data and Makes sharing this data with other applications easier. They encapsulate the data and act as an interface connecting data in one process with code running in another process, Each row represents an instance of data, and each column represents an individual piece of data for that instance

[!INFO] Who can access Content Providers?

  1. Your own app components - Activities, Services, Broadcast Receivers
  2. Other applications - If properly configured with permissions
  3. System apps - Android framework components like sync adapters
  4. Widgets - Home screen widgets that need app data
  5. Third-party apps - With appropriate permissions and export settings

What is a File Provider?
#

A “file provider” is not a separate Android component. Instead, It’s a Content Provider that is specifically designed and configured to manage and share file-based data.

What Content Providers Look Like
#

Core Components
#

1. Provider Client (ContentResolver)
#

Accessing data from a content provider

When an application wants to access data in a content provider, it uses the ContentResolver object available in its Context. The ContentResolver communicates with the provider object, performs the requested action, and returns results.

Basic CRUD Operations:

// Get ContentResolver
ContentResolver resolver = getContentResolver();

// Query data
Cursor cursor = resolver.query(
    uri,              // Content URI
    projection,       // Columns to return
    selection,        // WHERE clause
    selectionArgs,    // WHERE clause arguments
    sortOrder         // Sort order
);

// Insert data
ContentValues values = new ContentValues();
values.put("column_name", "value");
Uri newUri = resolver.insert(uri, values);

// Update data
int rowsUpdated = resolver.update(uri, values, selection, selectionArgs);

// Delete data
int rowsDeleted = resolver.delete(uri, selection, selectionArgs);

Bonus query code
#

This will return all data that the uri can access in database

Cursor cursor = getContentResolver().query(
   Uri.parse("content://io.hextree.flag30/success"), 
   null, null,
   null, null
);

// dump Uri

if (cursor!=null && cursor.moveToFirst()) {
    do {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < cursor.getColumnCount(); i++) {
            if (sb.length() > 0) {
                sb.append(", ");
            }
            sb.append(cursor.getColumnName(i) + " = " + cursor.getString(i));
        }
        Log.d("evil", sb.toString());
    } while (cursor.moveToNext());
}

2. Provider (ContentProvider subclass)
#

The actual implementation that manages access to data

public class MyContentProvider extends ContentProvider {
    
    @Override
    public boolean onCreate() {
        // Initialize provider (keep it fast!)
        return true;
    }
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, 
                       String[] selectionArgs, String sortOrder) {
        // Return data based on query
        return cursor;
    }
    
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // Insert new data and return URI
        return newUri;
    }
    
    @Override
    public int update(Uri uri, ContentValues values, String selection, 
                     String[] selectionArgs) {
        // Update existing data
        return rowsAffected;
    }
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // Delete data
        return rowsDeleted;
    }
    
    @Override
    public String getType(Uri uri) {
        // Return MIME type for the URI
        return mimeType;
    }
}

Built-in Content Providers
#

Android includes many system content providers including :

  • User Dictionary Provider: Non-standard words for spellcheck
  • Contacts Provider: User contact information
  • MediaStore: Images, videos, audio files on device
  • Calendar Provider: Calendar events and data

Creating Custom Content Providers
#

1. Design Data Storage
#

For structured data:

  • Decide storage mechanism (SQLite, files, etc.)
  • Data must have primary key column (often BaseColumns._ID)

For file data (unstructured):

  • Use file-oriented APIs
  • You can mix and match different storage types and expose them through a single content provider

Example of a DB that is prover can query
#

public class FlagDatabaseHelper extends SQLiteOpenHelper {
    public static final String COLUMN_CONTENT = "content";
    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_NAME = "name";
    public static final String COLUMN_TITLE = "title";
    public static final String COLUMN_VALUE = "value";
    public static final String COLUMN_VISIBLE = "visible";
    private static final String CREATE_TABKE_NOTE = "CREATE TABLE Note (_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL );";
    private static final String CREATE_TABLE_FLAG = "CREATE TABLE Flag (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, value TEXT NOT NULL, visible INTEGER NOT NULL DEFAULT 1);";
    private static final String DATABASE_NAME = "flag.db";
    private static final int DATABASE_VERSION = 1;
    public static final String TABLE_FLAG = "Flag";
    public static final String TABLE_NOTE = "Note";

    public FlagDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, (SQLiteDatabase.CursorFactory) null, 1);
    }

    @Override // android.database.sqlite.SQLiteOpenHelper
    public void onCreate(SQLiteDatabase sQLiteDatabase) {
        Log.i("FlagDatabaseHelper", "database created");
        sQLiteDatabase.execSQL(CREATE_TABLE_FLAG);
        sQLiteDatabase.execSQL(CREATE_TABKE_NOTE);
        sQLiteDatabase.execSQL("INSERT INTO Flag (name, value, visible) VALUES ('flag30', 'HXT{censored}', 1);");
        sQLiteDatabase.execSQL("INSERT INTO Flag (name, value, visible) VALUES ('flag31', 'HXT{censored}', 1);");
        sQLiteDatabase.execSQL("INSERT INTO Flag (name, value, visible) VALUES ('flag32', 'HXT{censored}', 0);");
        sQLiteDatabase.execSQL("INSERT INTO Note (title, content) VALUES ('secret', 'This is a secret note');");
        sQLiteDatabase.execSQL("INSERT INTO Note (title, content) VALUES ('flag33', 'HXT{censored}');");
    }

    @Override // android.database.sqlite.SQLiteOpenHelper
    public void onUpgrade(SQLiteDatabase sQLiteDatabase, int i, int i2) {
        sQLiteDatabase.execSQL("DROP TABLE IF EXISTS Flag");
        onCreate(sQLiteDatabase);
    }

From here we can see names an columns and how many table we have

Database Structure
#

  1. Database Name: flag.db

  2. Tables:

    • Flag - Stores flag information

    • Note - Stores notes (potentially containing additional flags)

Flag Table Schema
#

ColumnTypeDescription
_idINTEGER (PK)Auto-incrementing ID
nameTEXTFlag name (e.g., flag30, flag31)
valueTEXTFlag value (e.g., HXT{censored})
visibleINTEGERVisibility flag (1 = visible, 0 = hidden)

Note Table Schema
#

ColumnTypeDescription
_idINTEGER (PK)Auto-incrementing ID
titleTEXTNote title
contentTEXTNote content

Flag Table (Initial Entries)
#

namevaluevisible
flag30HXT{censored}1 (visible)
flag31HXT{censored}1 (visible)
flag32HXT{censored}0 (hidden)

Note Table (Initial Entries)
#

titlecontent
secretThis is a secret note
flag33HXT{censored}

2. Implement ContentProvider
#

Create subclass implementing six abstract methods:

public class CustomProvider extends ContentProvider {
    
    // Must be thread-safe (except onCreate)
    @Override
    public boolean onCreate() {
        // Fast initialization only - defer heavy tasks
        return true;
    }
    // Implement CRUD operations...
    // (See code examples above)
}

3. Define Metadata
#

Create a contract class with constants:

public final class ProviderContract {
    public static final String AUTHORITY = "com.example.provider";
    public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
    
    public static final class TableName {
        public static final Uri URI = Uri.withAppendedPath(BASE_URI, "table");
        public static final String COLUMN_ID = "_id";
        public static final String COLUMN_NAME = "name";
    }
}
  • Authority (android:authorities): This is the symbolic name that uniquely identifies your provider within the system. Use reverse internet domain ownership (e.g., com.example.yourapp.provider) to avoid conflicts
  • Content URIs: URIs identify data in your provider. They combine the content:// scheme, the provider’s authority, and a path that points to a table or file (e.g., content://user_dictionary/words)
  • Handling Content URI IDs: By convention, content URIs can include an ID value appended to the path (e.g., content://user_dictionary/words/4) to refer to a single row. The UriMatcher class can be used to map URI patterns (using wildcards like * for any string and # for any numeric string) to integer values, allowing you to easily handle different types of URI requests in a switch statement

4. Manifest Registration
#

Register in AndroidManifest.xml with proper authorities and permissions.

Use Cases
#

1- Secure Data Sharing
#

They allow other applications to securely access and modify your app’s data with proper permission controls.

2- Data Abstraction
#

Content providers abstract away the details of underlying data storage. You can change your internal data storage implementation (e.g., from SQLite to files) without affecting other applications.

3- More Permission Control
#

Greater control over permissions for accessing data:

  • Restrict access to only your application
  • Grant blanket permission to other applications
  • Configure different permissions for reading vs writing data

4- Framework Integration
#

Several Android classes rely on ContentProvider:

  • AbstractThreadedSyncAdapter for server synchronization
  • CursorAdapter and CursorLoader for async UI data loading
  • Custom search suggestions implementation
  • Widget data exposure
  • Complex data copy/paste operations

File Provider Implementation
#

How Content Providers Handle Files
#

When a content provider is used to share files, it manages file data such as photos, audio, or videos. Rather than storing large file data directly in a table, it is recommended to store the data in a file (preferably in your application’s private space) and then provide indirect access or a handle to that file when requested by another application.

For content providers that offer files, you are expected to implement the getStreamTypes() method, which returns a string array of MIME types (e.g., “image/jpeg”, “image/png”) for the files your provider can return for a given content URI.

[!WARNING] External Storage Security If files are stored on external storage, they are typically public and world-readable by default, and a content provider cannot restrict access to them through its own permissions, as other applications can use different API calls to read and write them directly. To ensure control over access to your data, you should store it in internal files, SQLite databases, or cloud storage, and keep these private to your application.

Configuration & Permissions
#

Manifest Declaration
#

<provider
    android:name=".MyContentProvider"
    android:authorities="com.example.provider"
    android:exported="false"
    android:permission="com.example.READ_WRITE_PERMISSION"
    android:readPermission="com.example.READ_PERMISSION"
    android:writePermission="com.example.WRITE_PERMISSION"
    android:grantUriPermissions="true">
    
    <!-- Path-specific permissions -->
    <path-permission 
        android:path="/sensitive/*"
        android:readPermission="com.example.SENSITIVE_READ" />
        
    <!-- Temporary URI permissions -->
    <grant-uri-permission android:pathPattern="/temp/*" />
</provider>

Some Key Attributes
#

android:exported
#

  • Default behavior changed in API 17: Defaults to false if no intent filters
  • If intent filters are defined, defaults to true unless explicitly set to false
  • External calls are blocked by activity manager if not exported (unless calling process is root/system)

Permission Levels
#

  1. Provider-level:
    • android:permission (single read/write)
    • android:readPermission / android:writePermission (separate permissions)
    • Separate permissions take precedence over single permission
  2. Path-level:
    • <path-permission> child elements
    • Apply to specific content URI paths
    • Take precedence over provider-level permissions

Temporary URI Permissions
#

  • These grant temporary access to a specific content URI, reducing the need for an app to request permanent permissions in its manifest.
  • Enabled by android:grantUriPermissions="true" or <grant-uri-permission>
  • Granted via setting intent flags to: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION when sending the content URI to another application.
  • Automatically revoked when receiving activity finishes

Data Types and MIME Types
#

Supported Data Types
#

Content providers can manage various data storage sources:

  • Structured data (like a SQLite relational database)
  • Unstructured data such as image files, audio, or video media
  • Binary Large Objects (BLOBs) - implemented as byte arrays
  • For file-oriented data (images/videos), store in files and provide indirect access

MIME Type Conventions
#

// Standard MIME types for common data
"text/html"
"image/jpeg" 

// Android vendor-specific MIME types for table data
// Multiple rows
"vnd.android.cursor.dir/vnd.com.example.provider.table1"

// Single row  
"vnd.android.cursor.item/vnd.example.line2"

// Get MIME type programmatically
String mimeType = getContentResolver().getType(uri);

Common Vulnerabilities
#

1. Overly open Access Controls
#

Vulnerability
#

  • Default exported state: Historically public by default (changed in API 17)
  • Missing permissions: No read/write permissions set
  • Inadequate permissions: Generic permissions for sensitive data

Vulnerable Code Example
#

// Manifest: AndroidManifest.xml
/*
<provider
    android:name=".MyContentProvider"
    android:authorities="com.example.myapp.provider"
    android:exported="true" />
*/
public class MyContentProvider extends ContentProvider {
    private DatabaseHelper dbHelper;

    @Override
    public boolean onCreate() {
        dbHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // Vulnerable: No permission checks, exposes sensitive data to any app
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        return db.query("sensitive_table", projection, selection, selectionArgs, null, null, sortOrder);
    }

    // Other required methods (insert, update, delete, getType) omitted for brevity
}

Mitigation
#

<!-- Explicitly set exported and permissions -->
<provider
    android:name=".MyProvider"
    android:authorities="com.example.provider"
    android:exported="false"
    android:permission="com.example.permission.SIGNATURE_REQUIRED" />
    
<!-- Use signature-level protection for sensitive data -->
<permission 
    android:name="com.example.permission.SIGNATURE_REQUIRED"
    android:protectionLevel="signature" />

2. Improperly Exposed Directories to FileProvider
#

Vulnerability
#

An improperly configured FileProvider can expose files and directories to an attacker. This often occurs when the FileProvider configuration uses broad path elements, such as <root-path>, which corresponds to the device’s root directory (/), or shares a wide path range like . or /.

  • Using root-path allowing arbitrary file access
  • Sharing entire private directories (files, cache)
  • General-purpose providers instead of specific ones

Attack
#

Allowing <root-path> grants arbitrary access to files and folders, including an app’s sandbox and /sdcard directory, presenting a broad attack surface. This can enable an attacker to access sensitive information stored in databases or overwrite the application’s native libraries, potentially leading to arbitrary code execution.

Vulnerable Code Example
#

// res/xml/file_paths.xml:
/*
<paths>
    <root-path name="root" path="" />
</paths>
*/
// Manifest: AndroidManifest.xml
/*
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.myapp.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
*/
public class FileShareActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        File file = new File("/path/to/file");
        // Vulnerable: Broad path exposes entire filesystem
        Uri uri = FileProvider.getUriForFile(this, "com.example.myapp.fileprovider", file);
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setData(uri);
        shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(shareIntent);
    }
}

Mitigation
#

<!-- file_paths.xml - SECURE configuration -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Specific subdirectory only -->
    <cache-path name="shared_images" path="images/" />
    <!-- NOT entire cache: <cache-path name="cache" path="." /> -->
</paths>

<!-- Manifest -->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

Additional Mitigations:

  • Do not use the <root-path> path element: This element grants arbitrary access to the entire device’s root directory
  • Share narrow path ranges: Instead of broad path ranges like . or /, specify limited and narrow paths in the FileProvider configuration
  • Grant minimum access permissions: When granting content URI permissions, ensure only the minimum necessary access is given
  • Avoid <external-path> for sensitive data: Sensitive data should not be stored in external storage accessible via <external-path>

3. Path Traversal when using data from Uri
#

Vulnerability
#

This common mistake occurs when developers use data from Uri methods like Uri.getLastPathSegment() or Uri.getPathSegments() without proper validation before passing it to file system APIs. These methods decode URL-encoded values, which attackers can exploit.

  • Using Uri.getLastPathSegment() without validation
  • URL-decoded values allowing ..%2F injection
  • Trusting ContentProvider-provided filenames

Attack
#

An attacker can provide a URL-encoded path traversal sequence (e.g., %2F for /) within a URI. When the vulnerable app decodes and uses this URI segment, it can be tricked into accessing or modifying files outside the intended directory. For example, an attacker could craft a URI like content://com.victim.path_traversal/..%2Fshared_prefs%2Fsecrets.xml to retrieve the contents of the secrets.xml file, which is typically stored in a private directory.

Vulnerable Code Example
#

public class FileAccessActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Uri uri = getIntent().getData();
        if (uri != null) {
            // Vulnerable: No validation of fileName allows path traversal
            String fileName = uri.getLastPathSegment();
            File file = new File(getFilesDir(), fileName);
            if (file.exists()) {
                try {
                    FileInputStream fis = new FileInputStream(file);
                    // Process file
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Mitigation
#

// SECURE: Validate file paths
private File getSecureFile(Uri uri) {
    String filename = uri.getLastPathSegment();
    if (filename == null) return null;
    
    // Re-encode to prevent path traversal
    filename = Uri.encode(filename);
    File file = new File(getSecureDirectory(), filename);
    
    try {
        // Validate canonical path is within expected directory
        String canonicalPath = file.getCanonicalPath();
        String secureDir = getSecureDirectory().getCanonicalPath();
        
        if (!canonicalPath.startsWith(secureDir)) {
            throw new SecurityException("Path traversal attempt detected");
        }
        
        return file;
    } catch (IOException e) {
        return null;
    }
}

// Generate unique filenames instead of trusting input
private String generateUniqueFilename(String extension) {
    return UUID.randomUUID().toString() + "." + extension;
}

Additional Mitigations:

  • Validate the resulting path: Canonicalize the path using File.getCanonicalPath() and compare its prefix with the expected safe directory
  • Implement additional validation: Include checks to prevent accidental overwrites and confirm operations occur in the expected directory
  • Avoid sharing broad folders in Content Providers: Ensure Content Providers do not expose “broad” folders like files or cache

4. Trusting ContentProvider-Provided Filename
#

Vulnerability
#

If a client application doesn’t correctly handle a filename provided by a FileProvider, a malicious application can implement its own FileProvider to provide a crafted filename.

Attack
#

A malicious FileProvider can supply a filename that includes path traversal characters (e.g., ../). When the victim client application attempts to write the received file to its storage using this untrusted filename, it might overwrite its own critical files, such as application code, shared preferences, or other configuration files, potentially leading to malicious code execution or altered application behavior.

Vulnerable Code Example
#

public class FileWriteActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Uri uri = getIntent().getData();
        if (uri != null) {
            // Vulnerable: Uses untrusted filename directly
            String fileName = uri.getLastPathSegment();
            File outputFile = new File(getFilesDir(), fileName);
            try {
                OutputStream os = new FileOutputStream(outputFile);
                // Write data to outputFile
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

Mitigation
#

  • Don’t trust user input for filenames: When a client application writes a received file, it should ignore the filename provided by the “server” application and instead generate its own unique filename
  • Sanitize provided filenames (less desirable): If unique filenames cannot be generated, sanitize the provided filename by removing path traversal characters and performing canonicalization

5. Exploiting Implicit Intents for File Theft and Overwriting
#

Vulnerability
#

Applications often launch implicit intents (e.g., ACTION_PICK, GET_CONTENT, IMAGE_CAPTURE) to interact with other apps, such as file managers or camera apps, to obtain a URI to a file. If the vulnerable app then copies the content from this URI to its public storage or processes it insecurely, it can be exploited.

Attack for File Theft
#

A malicious app can register an intent-filter with a high priority (android:priority="999") to intercept these implicit intents. Instead of providing a legitimate file, the malicious app returns a file:// URI that points to a sensitive file within the victim app’s private directory. When the victim app receives this malicious URI and attempts to copy its content, it unknowingly copies its own private data to a public directory, which the attacker can then read.

Attack for Arbitrary File Overwriting
#

Similarly, an attacker can use a malicious ContentProvider to return a filename that contains path traversal. If the vulnerable app copies the content using this filename, it could write arbitrary data to a sensitive location, like a native library file (.so), which could lead to arbitrary code execution within the victim app’s context.

Vulnerable Code Example
#

public class FilePickerActivity extends Activity {
    private static final int REQUEST_CODE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");
        startActivityForResult(intent, REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
            Uri uri = data.getData();
            // Vulnerable: No validation of URI source
            File publicFile = new File(Environment.getExternalStorageDirectory(), "public_file");
            try {
                InputStream is = getContentResolver().openInputStream(uri);
                OutputStream os = new FileOutputStream(publicFile);
                byte[] buffer = new byte[1024];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                is.close();
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Mitigation
#

  • Use internal storage for sensitive data: Always store sensitive application data in internal storage, which is restricted to the owning application
  • Avoid public file storage: Do not use public file storage for caching or any other operations with sensitive data
  • Encrypt sensitive data: If sensitive data must be stored on external storage, it should be encrypted using a strong algorithm
  • Perform integrity checks: For data or code loaded from external storage, implement integrity checks using hashes
  • Make intents explicit: Unless absolutely required, use explicit intents by calling setPackage()
  • Omit sensitive information from implicit intents: Do not include sensitive information or mutable objects
  • Validate external URIs: Check URIs from file:// or content:// schemes to ensure they do not point to local private files

6. Proxying Requests to More Secure Providers
#

Vulnerability
#

  • Lower-permission provider proxying to higher-permission provider
  • Dynamic URIs allowing attacker control over target provider

Note on Vulnerable Code Example
#

The vulnerable code example for this vulnerability is complex and typically involves multiple ContentProviders. It involves a less secure provider forwarding requests to a secure one without proper authorization.

Mitigation
#

// SECURE: Validate incoming URIs with allowlist
private static final Set<String> ALLOWED_AUTHORITIES = Set.of(
    "com.example.safe.provider1",
    "com.example.safe.provider2"
);

private boolean isUriSafe(Uri uri) {
    String authority = uri.getAuthority();
    return ALLOWED_AUTHORITIES.contains(authority);
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                   String[] selectionArgs, String sortOrder) {
    
    if (!isUriSafe(uri)) {
        throw new SecurityException("Unauthorized provider access attempt");
    }
    
    // Safe to proceed...
}

7. Theft of Arbitrary Files via File Choosers in WebView
#

Vulnerability
#

If an app implements WebChromeClient.onShowFileChooser() to allow users to select files from their device, but without proper validation of the chosen file’s URI, it can be vulnerable.

Attack
#

An attacker can provide a specially crafted URL to the WebView that includes an <input type="file"> element. The attacker’s app can then intercept the implicit intent launched by onShowFileChooser() and return the URI of a protected file from the victim’s private storage. The WebView then receives this URI and can potentially leak the content of the protected file.

Vulnerable Code Example
#

public class WebViewActivity extends Activity {
    private static final int REQUEST_CODE = 1;
    private ValueCallback<Uri[]> filePathCallback;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WebView webView = new WebView(this);
        setContentView(webView);
        webView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
                // Vulnerable: No validation of chosen file
                WebViewActivity.this.filePathCallback = filePathCallback;
                Intent intent = fileChooserParams.createIntent();
                startActivityForResult(intent, REQUEST_CODE);
                return true;
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
            Uri result = data.getData();
            filePathCallback.onReceiveValue(new Uri[]{result});
        }
    }
}

Mitigation
#

  • Disable file access in WebSettings: Set WebSettings.setAllowFileAccess(false), WebSettings.setAllowFileAccessFromFileURLs(false), and WebSettings.setAllowUniversalAccessFromFileURLs(false)
  • Disable content access in WebSettings: Call WebSettings.setAllowContentAccess(false)
  • Use WebViewAssetLoader: This method provides a secure way to access local files via an http(s):// scheme
  • Validate all URLs and origins: When loading external links in WebView, rigorously validate both the scheme and host
  • Sanitize JavaScript with external data: Ensure any externally obtained data used with JavaScript has been properly sanitized
  • Prevent WebView from loading untrusted content: If JavaScript execution and file access are enabled, strictly limit content loaded to trusted URLs

8. Gaining Access via Intent Redirection to Protected Components
#

Vulnerability
#

An app with an exported activity that takes an Intent from outside and returns it via Activity.setResult() without filtering unsafe flags, can be forced to grant permissions to Content Providers with the android:grantUriPermissions="true" flag.

Attack
#

An attacker sends an intent to this vulnerable activity. This intent includes a data URI pointing to the target ContentProvider and sets flags like Intent.FLAG_GRANT_READ_URI_PERMISSION. Because the vulnerable activity automatically returns the received intent (with its flags) via setResult(), the attacker’s app receives this modified intent back, now possessing the granted read permission for the specified URI. This allows the attacker to steal or rewrite protected or arbitrary files belonging to the victim application.

Vulnerable Code Example
#

public class VulnerableActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Vulnerable: Returns intent without filtering flags
        Intent intent = getIntent();
        setResult(RESULT_OK, intent);
        finish();
    }
}

Mitigation
#

  • Never redirect Intents in full: Instead of returning an Intent entirely, filter it to include only necessary data and remove any unsafe flags
  • Filter intent flags: Ensure that flags like FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION are handled securely
  • Validate intent components: When an Intent is created from a URL in WebView, reset the component and selector fields
  • Check if activity is exported: Before launching an activity from a URL-derived Intent, verify that the target activity is explicitly exported
  • Don’t export sensitive components: Avoid exporting components that access sensitive resources unless absolutely necessary
  • Require permissions for sensitive tasks: For any exported component performing sensitive tasks, explicitly require appropriate permissions
  • Apply signature-based permissions: When sharing data between your own ecosystem of apps, use signature protection level permissions
  • Implement single-task endpoints: Design components to perform a small, specific set of tasks with granular privileges

9. SQL Injection in Shared Databases
#

Vulnerability
#

  • Multiple providers sharing same SQLite database
  • SQL injection in less secure provider affecting secure provider’s data

Vulnerable Code Example
#

public class VulnerableContentProvider extends ContentProvider {
    private DatabaseHelper dbHelper;

    @Override
    public boolean onCreate() {
        dbHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // Vulnerable: Uses raw SQL with untrusted input
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        return db.rawQuery("SELECT * FROM table WHERE " + selection, selectionArgs);
    }

    // Other required methods omitted for brevity
}

Mitigation
#

// SECURE: Use separate databases for different security levels
public class SecureProvider extends ContentProvider {
    private SQLiteDatabase mSecureDb;
    
    @Override
    public boolean onCreate() {
        SQLiteOpenHelper helper = new MySecureDbHelper(getContext(), "secure.db");
        mSecureDb = helper.getWritableDatabase();
        return true;
    }
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                       String[] selectionArgs, String sortOrder) {
        
        // Use parameterized queries
        SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
        builder.setStrict(true);
        builder.setStrictColumns(true);
        builder.setStrictGrammar(true);
        
        return builder.query(mSecureDb, projection, selection, 
                           selectionArgs, null, null, sortOrder);
    }
}

10. Zip Path Traversal (ZipSlip)
#

Vulnerability
#

When an Android app extracts files from compressed archives (like ZIP files), if the extraction logic doesn’t validate the filenames within the archive for directory traversal characters (../), it can be vulnerable.

Attack
#

An attacker crafts a malicious ZIP file where entries have filenames like ../../../../data/data/com.victim/files/sensitive.txt. When the vulnerable app unpacks this archive, it could write arbitrary files to locations outside the intended destination directory, potentially overwriting application configuration files, databases, or even native libraries, leading to code execution.

Vulnerable Code Example
#

public class ZipExtractor {
    public void extractZip(File zipFile, File outputDir) throws IOException {
        ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
        ZipEntry entry;
        while ((entry = zis.getNextEntry()) != null) {
            // Vulnerable: No validation of entry name
            String entryName = entry.getName();
            File outputFile = new File(outputDir, entryName);
            OutputStream os = new FileOutputStream(outputFile);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = zis.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            os.close();
            zis.closeEntry();
        }
        zis.close();
    }
}

Mitigation
#

  • Verify target path is a child of destination directory: Before extracting each entry from a ZIP archive, always verify that the target path is a child of the intended destination directory by comparing their canonical paths
  • Ensure destination directory is empty: To prevent accidentally overwriting existing files, make sure the extraction destination directory is empty before starting the extraction process

Security & Best Practices
#

Permission Strategy
#

[!IMPORTANT] Principle of Least Privilege Only request necessary permissions and avoid being “overzealous”. Users implicitly grant requested permissions during installation.

Restrict Access Through Permissions
#

<!-- Example secure provider configuration -->
<provider
    android:name=".SecureProvider"
    android:authorities="com.example.secure.provider"
    android:exported="true"
    android:readPermission="com.example.permission.READ_DATA"
    android:writePermission="com.example.permission.WRITE_DATA">
    
    <!-- Extra protection for sensitive paths -->
    <path-permission 
        android:path="/admin/*"
        android:readPermission="com.example.permission.ADMIN_READ"
        android:writePermission="com.example.permission.ADMIN_WRITE" />
</provider>

<!-- Define custom permissions -->
<permission 
    android:name="com.example.permission.READ_DATA"
    android:protectionLevel="signature" />

Secure Data Storage
#

[!WARNING] Keep Data Private Store data in internal files, SQLite databases, or cloud and keep these private to your application.

  • Avoid world-readable/writable: Don’t set internal files or SQLite databases to world-readable/writable
  • SQLite databases are private by default
  • This prevents permissions bypass

Prevent SQL Injection
#

[!DANGER] SQL Injection Prevention Always sanitize user input when querying, inserting, updating, or deleting data.

Recommended approach:

// SECURE: Use selection with replaceable parameters
String selection = "column = ?";
String[] selectionArgs = {"user_input"};
Cursor cursor = resolver.query(uri, projection, selection, selectionArgs, sortOrder);

// INSECURE: Direct string concatenation
String selection = "column = '" + userInput + "'"; // DON'T DO THIS!

Additional protections:

  • Use PreparedStatement objects
  • Use Android’s built-in query() methods
  • Configure SQLiteQueryBuilder with setStrict(), setStrictColumns(), setStrictGrammar()
  • Consider Room Persistence Library for compile-time SQL verification

Implementation Best Practices
#

Thread Safety
#

[!NOTE] Thread Safety All ContentProvider methods except onCreate() can be called by multiple threads simultaneously - implementations must be thread-safe.

Efficient onCreate()
#

@Override
public boolean onCreate() {
    // Only fast-running initialization here
    // Defer heavy tasks like database creation until needed
    // This prevents slowing down provider startup
    return true;
}

Contract Classes
#

// Define constants in public final contract class
public final class MyContract {
    public static final String AUTHORITY = "com.example.provider";
    public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
    
    // Table contracts
    public static final class Users implements BaseColumns {
        public static final Uri URI = Uri.withAppendedPath(BASE_URI, "users");
        public static final String TABLE_NAME = "users";
        public static final String COLUMN_NAME = "name";
        public static final String COLUMN_EMAIL = "email";
    }
}

In The end
#

[!TIP] Key Takeaways

  1. Principle of Least Privilege - Only grant necessary permissions
  2. Defense in Depth - Multiple security layers (export, permissions, validation)
  3. Simplicity - Keep providers focused on data access only
  4. Validation - Always validate and sanitize input
  5. Separation - Isolate different security levels

Resources
#

Documentation
#

Security Resources
#

Best Practices
#


Reply by Email
Saeed Khalid
Author
Saeed Khalid
Just trying to have fun

Related

HEX Tree Content & File providers Challenges
·3749 words·18 mins
Ctf Andorid Contnet Provider File Provider Hextree
Broadcast
·3199 words·16 mins
Summary Andorid Broadcast
What is your (Intent)ion
·3947 words·19 mins
Summary Andorid Intent Pending Implicit Deep Links