מקודדי תוכן

קטגוריה ב-OWASP: MASVS-PLATFORM: Platform Interaction

סקירה כללית

לפי המסמכים, ContentResolver הוא 'כיתה שמספקת לאפליקציות גישה למודל התוכן'. פותרי תוכן חושפים שיטות ליצירת אינטראקציה עם תוכן, לאחזור תוכן או לשינוי תוכן שמסופק מהמקורות הבאים:

  • אפליקציות מותקנות (סכימת URI של content://)
  • מערכות קבצים (סכימת URI מסוג file://)
  • תמיכה בממשקי API כפי שסופקו על ידי Android (סכימת URI מסוג android.resource://).

לסיכום, נקודות החולשה שקשורות ל-ContentResolver שייכות לקטגוריה משרת מבולבל, כי התוקף יכול להשתמש בהרשאות של אפליקציה פגיעה כדי לגשת לתוכן מוגן.

סיכון: ניצול לרעה על סמך URI לא מהימן מסוג file:// ‎

ניצול לרעה של ContentResolver באמצעות נקודת החולשה של ה-URI file:// מנצל את היכולת של ContentResolver להחזיר מתארי קבצים שמתוארים על ידי ה-URI. נקודת החולשה הזו משפיעה על פונקציות כמו openFile(),‏ openFileDescriptor(),‏ openInputStream(),‏ openOutputStream() ו-openAssetFileDescriptor() מ-API של ContentResolver. אפשר לנצל לרעה את נקודת החולשה באמצעות URI של file:// שנמצא בשליטה מלאה או חלקית של תוקף, כדי לאלץ את האפליקציה לגשת לקבצים שלא נועדו להיות נגישים, כמו מסדי נתונים פנימיים או העדפות משותפות.

אחד מתרחישי ההתקפה האפשריים הוא יצירת גלריה או בורר קבצים זדוניים, שבשימוש באפליקציה פגיעה יחזירו 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 של content:// לא מהימן

ניצול לרעה של 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 שמוגן באמצעות הרשאות וששייך לאפליקציה אחרת שמאמינה באפליקציה הפגיעה.

ההתקפה הזו רלוונטית למקרים הבאים:

  • מערכות אקולוגיות של אפליקציות שבהן האפליקציות מגדירות הרשאות בהתאמה אישית או מנגנוני אימות אחרים ומשתמשות בהן.
  • התקפות proxy של הרשאות, שבהן תוקף מנצל לרעה אפליקציה פגיעה שיש לה הרשאת זמן ריצה, כמו 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;
}

אם השימוש בספקי תוכן אחרים לא מחייב מתן הרשאה – למשל, אם האפליקציה מאפשרת לכל האפליקציות מהסביבה העסקית לגשת לכל הנתונים – צריך לאסור במפורש את השימוש ברשויות האלה.