קטגוריה ב-OWASP: MASVS-PLATFORM: Platform Interaction
סקירה כללית
לפי המסמכים, ContentResolver הוא class שמאפשר לאפליקציות לגשת למודל התוכן. ContentResolvers חושף שיטות לאינטראקציה עם תוכן שסופק מהמקורות הבאים, לאחזור שלו או לשינוי שלו:
- אפליקציות מותקנות (סכמת URI
content://) - מערכות קבצים (סכמת URI
file://) - תמיכה בממשקי API כפי שמסופקים על ידי Android (סכימת URI
android.resource://).
לסיכום, פגיעויות שקשורות ל-ContentResolver שייכות לסיווג confused deputy, כי התוקף יכול להשתמש בהרשאות של אפליקציה פגיעה כדי לגשת לתוכן מוגן.
סיכון: ניצול לרעה על סמך URI לא מהימן של file://
ניצול לרעה של ContentResolver באמצעות פגיעות ב-URI של file:// מאפשר לנצל את היכולת של ContentResolver להחזיר מתארים של קבצים שמתוארים על ידי ה-URI. נקודת החולשה הזו משפיעה על פונקציות כמו openFile(), openFileDescriptor(), openInputStream(), openOutputStream() או openAssetFileDescriptor() מ-ContentResolver API. אפשר לנצל את נקודת החולשה באמצעות file:// URI שנשלט באופן מלא או חלקי על ידי התוקף, כדי לאלץ את האפליקציה לגשת לקבצים שלא אמורה להיות להם גישה אליהם, כמו מסדי נתונים פנימיים או העדפות משותפות.
אחד מתרחישי התקיפה האפשריים הוא יצירת גלריה או כלי לבחירת קבצים זדוניים, שכשמשתמשים בהם באפליקציה פגיעה, מחזירים URI זדוני.
יש כמה גרסאות של המתקפה הזו:
- כתובת 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.
}
סיכון: ניצול לרעה על סמך URI של תוכן לא מהימן://
ניצול לרעה של ContentResolver באמצעות פגיעות ב-URI של content:// מתרחש כשמזהה URI שנשלט באופן מלא או חלקי על ידי תוקף מועבר לממשקי API של ContentResolver כדי לבצע פעולות על תוכן שלא נועד להיות נגיש.
יש שני תרחישים עיקריים למתקפה הזו:
- האפליקציה פועלת על תוכן פנימי משלה. לדוגמה: אחרי קבלת URI מתוקף, אפליקציית הדואר מצרפת נתונים מספק התוכן הפנימי שלה במקום תמונה חיצונית.
- האפליקציה פועלת כשרת proxy ואז ניגשת לנתונים של אפליקציה אחרת בשביל התוקף. לדוגמה: אפליקציית הדואר מצרפת נתונים מאפליקציה X שמוגנים על ידי הרשאה שבדרך כלל לא מאפשרת לתוקף לראות את הקובץ המצורף הספציפי הזה. היא זמינה לאפליקציה שמבצעת את הצירוף, אבל לא באופן ראשוני, ולכן היא מעבירה את התוכן הזה לתוקף.
תרחיש אפשרי אחד של מתקפה הוא יצירת גלריה זדונית או כלי לבחירת קבצים, שאם אפליקציה פגיעה תשתמש בהם, הם יחזירו URI זדוני.
השפעה
ההשפעה של ניצול הפגיעות הזו משתנה בהתאם להקשר שמשויך ל-ContentResolver. הדבר עלול לגרום להעברה לא מורשית של נתונים מוגנים מאפליקציה או לשינויים בנתונים מוגנים על ידי גורמים לא מורשים.
אמצעי צמצום סיכונים
כללי
אימות של מזהי URI נכנסים. לדוגמה, שימוש ברשימת היתרים של רשויות צפויות נחשב לשיטה מומלצת.
יעד ה-URI הוא ספק תוכן שלא מיוצא או שמוגן בהרשאה, ושייך לאפליקציה פגיעה
בודקים אם ה-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;
}
אם השימוש בספקי תוכן אחרים לא דורש הענקת הרשאה – למשל, אם האפליקציה מאפשרת לכל האפליקציות מהמערכת האקולוגית לגשת לכל הנתונים – אז צריך לאסור במפורש את השימוש בסמכויות האלה.