טעינה של קוד דינמי

קטגוריה ב-OWASP: MASVS-CODE: איכות הקוד

סקירה כללית

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

צורות רבות של טעינת קוד דינמי, במיוחד כאלה שמשתמשות במקורות מרוחקים, מפירות את המדיניות של Google Play ועשויות להוביל להשעיית האפליקציה מ-Google Play.

השפעה

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

פעולות מיטיגציה

הימנעות משימוש בטעינת קוד דינמי

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

שימוש במקורות מהימנים

קוד שיוטמע באפליקציה צריך להיות מאוחסן במיקומים מהימנים. לגבי אחסון מקומי, האחסון הפנימי של האפליקציה או האחסון המוגבל (ב-Android 10 ואילך) הם המקומות המומלצים. המיקומים האלה כוללים אמצעי הגנה כדי למנוע גישה ישירה מאפליקציות וממשתמשים אחרים.

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

ביצוע בדיקות תקינות

מומלץ לבצע בדיקות תקינות כדי לוודא שהקוד לא נפרץ. צריך לבצע את הבדיקות האלה לפני טעינת הקוד באפליקציה.

כשאתם מעמיסים משאבים מרוחקים, אתם יכולים להשתמש בתקינות של משאבי המשנה כדי לאמת את התקינות של המשאבים שאתם ניגשים אליהם.

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

Kotlin

package com.example.myapplication

import java.io.BufferedInputStream
import java.io.FileInputStream
import java.io.IOException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

object FileIntegrityChecker {
    @Throws(IOException::class, NoSuchAlgorithmException::class)
    fun getIntegrityHash(filePath: String?): String {
        val md = MessageDigest.getInstance("SHA-256") // You can choose other algorithms as needed
        val buffer = ByteArray(8192)
        var bytesRead: Int
        BufferedInputStream(FileInputStream(filePath)).use { fis ->
            while (fis.read(buffer).also { bytesRead = it } != -1) {
                md.update(buffer, 0, bytesRead)
            }

    }

    private fun bytesToHex(bytes: ByteArray): String {
        val sb = StringBuilder(bytes.length * 2)
        for (b in bytes) {
            sb.append(String.format("%02x", b))
        }
        return sb.toString()
    }

    @Throws(IOException::class, NoSuchAlgorithmException::class)
    fun verifyIntegrity(filePath: String?, expectedHash: String): Boolean {
        val actualHash = getIntegrityHash(filePath)
        return actualHash == expectedHash
    }

    @Throws(Exception::class)
    @JvmStatic
    fun main(args: Array<String>) {
        val filePath = "/path/to/your/file"
        val expectedHash = "your_expected_hash_value"
        if (verifyIntegrity(filePath, expectedHash)) {
            println("File integrity is valid!")
        } else {
            println("File integrity is compromised!")
        }
    }
}

Java

package com.example.myapplication;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class FileIntegrityChecker {

    public static String getIntegrityHash(String filePath) throws IOException, NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256"); // You can choose other algorithms as needed
        byte[] buffer = new byte[8192];
        int bytesRead;

        try (BufferedInputStream fis = new BufferedInputStream(new FileInputStream(filePath))) {
            while ((bytesRead = fis.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }
        }

        byte[] digest = md.digest();
        return bytesToHex(digest);
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public static boolean verifyIntegrity(String filePath, String expectedHash) throws IOException, NoSuchAlgorithmException {
        String actualHash = getIntegrityHash(filePath);
        return actualHash.equals(expectedHash);
    }

    public static void main(String[] args) throws Exception {
        String filePath = "/path/to/your/file";
        String expectedHash = "your_expected_hash_value";

        if (verifyIntegrity(filePath, expectedHash)) {
            System.out.println("File integrity is valid!");
        } else {
            System.out.println("File integrity is compromised!");
        }
    }
}

חתימה על הקוד

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

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

דוגמאות לחתימה על קוד מפורטות בקטע 'משאבים' במסמך הזה.

משאבים