فئة OWASP: MASVS-PLATFORM: التفاعل مع النظام الأساسي
نظرة عامة
وفقًا لـ الوثائق، ContentResolver هو "فئة تمنح التطبيقات إمكانية الوصول إلى نموذج المحتوى". تعرض ContentResolvers طرقًا للتفاعل مع المحتوى المقدَّم من المصادر التالية أو جلبه أو تعديله:
- التطبيقات المثبّتة (مخطّط معرّف الموارد المنتظم
content://) - أنظمة الملفات (مخطّط معرّف الموارد المنتظم
file://) - واجهات برمجة التطبيقات المتوافقة التي توفّرها Android (مخطّط معرّف الموارد المنتظم
android.resource://)
باختصار، تنتمي الثغرات الأمنية المرتبطة بـ ContentResolver إلى فئة النائب المضلَّل، إذ يمكن للمهاجم استخدام امتيازات تطبيق يتضمّن ثغرة أمنية للوصول إلى محتوى محمي.
الخطر: إساءة الاستخدام استنادًا إلى معرّف الموارد المنتظم `file:// ` غير الموثوق به
تستغل إساءة استخدام ContentResolver باستخدام الثغرة الأمنية لمعرّف الموارد المنتظم file:// قدرة ContentResolver على عرض واصفات الملفات التي يصفها معرّف الموارد المنتظم. تؤثر هذه الثغرة الأمنية في دوال مثل openFile() أو openFileDescriptor() أو openInputStream() أو openOutputStream() أو openAssetFileDescriptor() من ContentResolver API. يمكن إساءة استخدام الثغرة الأمنية باستخدام معرّف موارد منتظم file:// يتحكّم فيه المهاجم بشكل كامل أو جزئي لإجبار التطبيق على الوصول إلى ملفات لم يكن من المفترض أن تكون متاحة، مثل قواعد البيانات الداخلية أو الإعدادات المفضّلة المشترَكة.
أحد سيناريوهات الهجوم المحتملة هو إنشاء معرض صور أو أداة اختيار ملفات ضارة، وعندما يستخدمها تطبيق يتضمّن ثغرة أمنية، ستعرض معرّف موارد منتظم ضار.
هناك بعض أشكال هذا الهجوم:
- معرّف موارد منتظم
file://يتحكّم فيه المهاجم بشكل كامل ويشير إلى الملفات الداخلية لتطبيق - يتحكّم المهاجم في جزء من معرّف الموارد المنتظم
file://، ما يجعله عرضة لعمليات اجتياز المسار - معرّف موارد منتظم
file://يستهدف رابطًا رمزيًا (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 باستخدام الثغرة الأمنية لمعرّف الموارد المنتظم content:// عندما يتم تمرير معرّف موارد منتظم يتحكّم فيه المهاجم بشكل كامل أو جزئي إلى واجهات برمجة التطبيقات ContentResolver للتعامل مع محتوى لم يكن من المفترض أن يكون متاحًا.
هناك سيناريوهان رئيسيان لهذا الهجوم:
- يتعامل التطبيق مع المحتوى الداخلي الخاص به. على سبيل المثال: بعد الحصول على معرّف موارد منتظم من مهاجم، يرفق تطبيق البريد بيانات من موفّر المحتوى الداخلي الخاص به بدلاً من صورة خارجية.
- يعمل التطبيق كوكيل ثم يصل إلى بيانات تطبيق آخر نيابةً عن المهاجم. على سبيل المثال: يرفق تطبيق البريد بيانات من التطبيق "س" المحمية بإذن يمنع المهاجم عادةً من رؤية هذا المرفق المحدد. تكون البيانات متاحة للتطبيق الذي يرفقها، ولكن ليس في البداية، وبالتالي يتم نقل هذا المحتوى إلى المهاجم.
أحد سيناريوهات الهجوم المحتملة هو إنشاء معرض صور أو أداة اختيار ملفات ضارة، وعندما يستخدمها تطبيق يتضمّن ثغرة أمنية، ستعرض معرّف موارد منتظم ضار.
التأثير
يختلف تأثير استغلال هذه الثغرة الأمنية حسب السياق المرتبط بـ ContentResolver. قد يؤدي ذلك إلى تسريب البيانات المحمية لتطبيق أو إجراء تعديلات عليها من قِبل أطراف غير مصرّح لها.
الإجراءات المخفّفة
بنود عامة
تحقَّق من معرّفات الموارد المنتظمة الواردة. على سبيل المثال، يُعد استخدام قائمة السماح للسلطات المتوقّعة ممارسة جيدة.
يستهدف معرّف الموارد المنتظم موفّر محتوى غير مُصدَّر أو محمي بإذن وينتمي إلى تطبيق يتضمّن ثغرة أمنية
تحقَّق مما إذا كان معرّف الموارد المنتظم يستهدف تطبيقك:
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;
}
أو إذا تم منح إذن صريح لمعرّف الموارد المنتظم، يستند هذا التحقّق إلى افتراض أنّه إذا تم منح إذن صريح للوصول إلى البيانات، لن يكون معرّف الموارد المنتظم ضارًا:
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;
}
يستهدف معرّف الموارد المنتظم ContentProvider محميًا بإذن وينتمي إلى تطبيق آخر يثق في التطبيق الذي يتضمّن ثغرة أمنية.
ينطبق هذا الهجوم على الحالات التالية:
- الأنظمة الأساسية للتطبيقات التي تحدّد التطبيقات فيها أذونات مخصّصة أو آليات مصادقة أخرى وتستخدمها.
- هجمات وكيل الأذونات، حيث يسيء المهاجم استخدام تطبيق يتضمّن ثغرة أمنية ويحمل إذن التشغيل، مثل READ_CONTACTS، لاسترداد البيانات من موفّر النظام.
اختبِر ما إذا تم منح إذن معرّف الموارد المنتظم:
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;
}
إذا كان استخدام موفّري المحتوى الآخرين لا يتطلّب منح إذن، مثل عندما يسمح التطبيق لجميع التطبيقات من النظام الأساسي بالوصول إلى جميع البيانات، فامنَع صراحةً استخدام هذه السلطات.