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

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

סקירה כללית

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

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

השפעה

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

אמצעי צמצום סיכונים

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

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

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

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

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

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

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

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

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

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!");
        }
    }
}

חתימה על הקוד

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

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

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

משאבים