فئة OWASP: MASVS-PLATFORM: Platform Interaction (التفاعل مع النظام الأساسي)
نظرة عامة
وفقًا للمستندات، ContentResolver
هي "فئة توفّر للتطبيقات إمكانية الوصول إلى نموذج المحتوى". تعرِض فئات ContentResolver طرقًا للتفاعل مع المحتوى المقدَّم من المصادر التالية أو استرجاعه أو تعديله:
- التطبيقات المثبّتة (مخطّط
content://
URI) - أنظمة الملفات (
file://
نظام تعريف الموارد المنتظم) - واجهات برمجة التطبيقات المتوافقة كما يوفّرها Android (
android.resource://
مخطّط معرّف الموارد المنتظم)
باختصار، تنتمي الثغرات المتعلقة بـ ContentResolver
إلى فئة الوكيل المُربك لأنّ المهاجم يمكنه استخدام امتيازات تطبيق معيّن يتضمّن ثغرة للوصول إلى محتوى محمي.
الخطر: إساءة الاستخدام استنادًا إلى معرّف الموارد المنتظم (URI) غير الموثوق به file://
إنّ إساءة استخدام ContentResolver
باستخدام ثغرة معرّف الموارد المنتظم file://
تستغل قدرة ContentResolver
على عرض أوصاف الملفات الموضّحة في معرّف الموارد المنتظم. تؤثر هذه الثغرة الأمنية في دوال مثل openFile()
أو openFileDescriptor()
أو openInputStream()
أو openOutputStream()
أو openAssetFileDescriptor()
من ContentResolver
واجهة برمجة التطبيقات. يمكن إساءة استخدام هذه الثغرة من خلال عنوان file://
URI يتحكم فيه المهاجم كليًا أو جزئيًا لإجبار التطبيق على الوصول إلى الملفات التي لم يكن من المفترض الوصول إليها، مثل قواعد البيانات الداخلية أو الإعدادات المفضّلة المشتركة.
أحد سيناريوهات الهجوم المحتملة هو إنشاء معرض صور أو أداة اختيار ملفات ضارة، والتي عند استخدامها من قِبل تطبيق معرّض للاختراق، ستعرِض معرّف موارد منتظمًا ضارًا.
هناك بعض الصيغ لهذا الهجوم:
- معرّف
file://
URI يتحكم فيه المهاجم بالكامل ويشير إلى الملفات الداخلية للتطبيق - جزء من معرّف الموارد المنتظم
file://
يخضع للتحكّم من قِبل المهاجم، ما يجعله عرضة لعمليات اختراق المسار file://
معرّف URI يستهدف رابطًا رمزيًا (symlink) يتحكّم فيه المهاجم ويشير إلى الملفات الداخلية للتطبيق- يشبه هذا النوع النوع السابق، ولكنّ المهاجم هنا يبدّل بشكل متكرّر وجهة الرابط الرمزي من هدف مشروع إلى ملفات التطبيق الداخلية. والهدف من ذلك هو استغلال حالة تسابق بين عملية التحقّق من الأمان المحتمَلة واستخدام مسار الملف.
التأثير
يختلف تأثير استغلال هذه الثغرة الأمنية حسب الغرض من استخدام ContentResolver. في كثير من الحالات، يمكن أن يؤدي ذلك إلى إخراج البيانات المحمية من التطبيق أو تعديلها من قِبل جهات غير مصرَّح لها بذلك.
إجراءات التخفيف
للحدّ من هذه الثغرة الأمنية، استخدِم الخوارزمية أدناه للتحقّق من صحّة وصف الملف. بعد اجتياز عملية التحقّق، يمكن استخدام وصف الملف بأمان.
Kotlin
fun isValidFile(ctx: Context, pfd: ParcelFileDescriptor, fileUri: Uri): Boolean {
// Canonicalize to resolve symlinks and path traversals.
val fdCanonical = File(fileUri.path!!).canonicalPath
val pfdStat: StructStat = Os.fstat(pfd.fileDescriptor)
// Lstat doesn't follow the symlink.
val canonicalFileStat: StructStat = Os.lstat(fdCanonical)
// Since we canonicalized (followed the links) the path already,
// the path shouldn't point to symlink unless it was changed in the
// meantime.
if (OsConstants.S_ISLNK(canonicalFileStat.st_mode)) {
return false
}
val sameFile =
pfdStat.st_dev == canonicalFileStat.st_dev &&
pfdStat.st_ino == canonicalFileStat.st_ino
if (!sameFile) {
return false
}
return !isBlockedPath(ctx, fdCanonical)
}
fun isBlockedPath(ctx: Context, fdCanonical: String): Boolean {
// Paths that should rarely be exposed
if (fdCanonical.startsWith("/proc/") ||
fdCanonical.startsWith("/data/misc/")) {
return true
}
// Implement logic to block desired directories. For example, specify
// the entire app data/ directory to block all access.
}
Java
boolean isValidFile(Context ctx, ParcelFileDescriptor pfd, Uri fileUri) {
// Canonicalize to resolve symlinks and path traversals
String fdCanonical = new File(fileUri.getPath()).getCanonicalPath();
StructStat pfdStat = Os.fstat(pfd.getFileDescriptor());
// Lstat doesn't follow the symlink.
StructStat canonicalFileStat = Os.lstat(fdCanonical);
// Since we canonicalized (followed the links) the path already,
// the path shouldn't point to symlink unless it was changed in the meantime
if (OsConstants.S_ISLNK(canonicalFileStat.st_mode)) {
return false;
}
boolean sameFile =
pfdStat.stDev == canonicalFileStat.stDev && pfdStat.stIno == canonicalFileStat.stIno;
if (!sameFile) {
return false;
}
return !isBlockedPath(ctx, fdCanonical);
}
boolean isBlockedPath(Context ctx, String fdCanonical) {
// Paths that should rarely be exposed
if (fdCanonical.startsWith("/proc/") || fdCanonical.startsWith("/data/misc/")) {
return true;
}
// Implement logic to block desired directories. For example, specify
// the entire app data/ directory to block all access.
}
الخطر: إساءة الاستخدام استنادًا إلى معرّف الموارد المنتظم content:// غير الموثوق به
يحدث إساءة استخدام ContentResolver
باستخدام ثغرة في عنوان URL content://
عندما يتم تمرير عنوان URL يتحكّم فيه المهاجم كليًا أو جزئيًا إلى واجهات برمجة تطبيقات ContentResolver
لإجراء عمليات على محتوى لم يكن من المفترض أن يكون متاحًا.
هناك سيناريوهان رئيسيان لهذا الهجوم:
- يعمل التطبيق على المحتوى الداخلي الخاص به. على سبيل المثال: بعد الحصول على عنوان URI من مهاجم، يُرفِق تطبيق البريد بيانات من موفِّر المحتوى الداخلي بدلاً من صورة خارجية.
- يعمل التطبيق كوكيل، ثم يصل إلى بيانات تطبيق آخر للمهاجم. على سبيل المثال، يُرفِق تطبيق البريد بيانات من التطبيق "س" المحمية بإذن يمنع عادةً المهاجم من الاطّلاع على هذا المرفق المحدّد. ويتوفّر هذا المحتوى للتطبيق الذي يُجري عملية إرفاقه، ولكن ليس في البداية، وبالتالي لا يتم نقل هذا المحتوى إلى المهاجم.
أحد سيناريوهات الهجوم المحتملة هو إنشاء معرض صور أو أداة اختيار ملفات ضارة، والتي عند استخدامها من خلال تطبيق معرّض للاختراق، ستعرِض عنوان URL ضارًا.
التأثير
يختلف تأثير استغلال هذه الثغرة الأمنية حسب السياق المرتبط بـ ContentResolver. وقد يؤدي ذلك إلى تهريب البيانات المحمية في التطبيق أو إجراء تعديلات عليها من قِبل جهات غير مصرَّح لها بذلك.
إجراءات التخفيف
بنود عامة
التحقّق من صحة عناوين URL الواردة على سبيل المثال، يُعدّ استخدام قائمة مسموح بها للجهات المعتمَدة من الممارسات الجيدة.
يستهدف عنوان URI موفّر محتوى غير مُصدَّر أو محمي بإذن وينتمي إلى تطبيق معرّض للاختراق.
تأكَّد مما إذا كان معرّف الموارد المنتظم يستهدف تطبيقك:
Kotlin
fun belongsToCurrentApplication(ctx: Context, uri: Uri): Boolean {
val authority: String = uri.authority.toString()
val info: ProviderInfo =
ctx.packageManager.resolveContentProvider(authority, 0)!!
return ctx.packageName.equals(info.packageName)
}
Java
boolean belongsToCurrentApplication(Context ctx, Uri uri){
String authority = uri.getAuthority();
ProviderInfo info = ctx.getPackageManager().resolveContentProvider(authority, 0);
return ctx.getPackageName().equals(info.packageName);
}
أو إذا تم تصدير مقدّم الخدمة المستهدَف:
Kotlin
fun isExported(ctx: Context, uri: Uri): Boolean {
val authority = uri.authority.toString()
val info: ProviderInfo =
ctx.packageManager.resolveContentProvider(authority, 0)!!
return info.exported
}
Java
boolean isExported(Context ctx, Uri uri){
String authority = uri.getAuthority();
ProviderInfo info = ctx.getPackageManager().resolveContentProvider(authority, 0);
return info.exported;
}
أو إذا تم منح إذن صريح لعنوان URI، يستند هذا التحقّق إلى الافتراض بأنّه إذا تم منح إذن صريح للوصول إلى البيانات، لن يكون عنوان URI ضارًا:
Kotlin
// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
fun wasGrantedPermission(ctx: Context, uri: Uri?, grantFlag: Int): Boolean {
val pid: Int = Process.myPid()
val uid: Int = Process.myUid()
return ctx.checkUriPermission(uri, pid, uid, grantFlag) ==
PackageManager.PERMISSION_GRANTED
}
Java
// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
boolean wasGrantedPermission(Context ctx, Uri uri, int grantFlag){
int pid = Process.myPid();
int uid = Process.myUid();
return ctx.checkUriPermission(uri, pid, uid, grantFlag) == PackageManager.PERMISSION_GRANTED;
}
يستهدف عنوان URI فئة ContentProvider محمية بإذن وتعود ملكيتها لتطبيق آخر يثق بالتطبيق المعنيّ بالثغرة الأمنية.
ينطبق هذا الهجوم على الحالات التالية:
- الأنظمة المتكاملة للتطبيقات التي تحدّد التطبيقات فيها الأذونات المخصّصة أو آليات المصادقة الأخرى وتستخدمها
- هجمات إذن الوكيل، حيث يسيء المهاجم استخدام تطبيق معرّض للاختراق يمتلك إذن وقت التشغيل، مثل READ_CONTACTS، لاسترداد البيانات من موفّر نظام
يمكنك اختبار ما إذا تم منح إذن URI:
Kotlin
// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
fun wasGrantedPermission(ctx: Context, uri: Uri?, grantFlag: Int): Boolean {
val pid: Int = Process.myPid()
val uid: Int = Process.myUid()
return ctx.checkUriPermission(uri, pid, uid, grantFlag) ==
PackageManager.PERMISSION_GRANTED
}
Java
// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
boolean wasGrantedPermission(Context ctx, Uri uri, int grantFlag){
int pid = Process.myPid();
int uid = Process.myUid();
return ctx.checkUriPermission(uri, pid, uid, grantFlag) == PackageManager.PERMISSION_GRANTED;
}
إذا كان استخدام مقدّمي المحتوى الآخرين لا يتطلّب منح الإذن، مثل عندما يسمح التطبيق لجميع التطبيقات من المنظومة المتكاملة بالوصول إلى جميع البيانات، يجب حظر استخدام هذه الأذونات صراحةً.