การเปิดเผยข้อมูลบันทึก

หมวดหมู่ OWASP: MASVS-STORAGE: Storage

ภาพรวม

การเปิดเผยข้อมูลบันทึกเป็นช่องโหว่ประเภทหนึ่งที่แอปจะพิมพ์ข้อมูลที่ละเอียดอ่อนลงในบันทึกของอุปกรณ์ หากผู้ไม่ประสงค์ดีเข้าถึงข้อมูลที่มีความละเอียดอ่อนนี้ได้ ข้อมูลดังกล่าวอาจมีค่าในทันที เช่น ข้อมูลเข้าสู่ระบบของผู้ใช้หรือข้อมูลส่วนบุคคลที่ระบุตัวบุคคลนั้นได้ (PII) หรืออาจทำให้เกิดการโจมตีเพิ่มเติม

ปัญหานี้อาจเกิดขึ้นในสถานการณ์ต่อไปนี้

  • บันทึกที่แอปสร้างขึ้น
    • บันทึกจงใจอนุญาตให้ผู้กระทำการที่ไม่ได้รับอนุญาตเข้าถึง แต่กลับมีข้อมูลที่ละเอียดอ่อนโดยไม่ตั้งใจ
    • บันทึกมีข้อมูลที่ละเอียดอ่อนโดยเจตนา แต่ผู้ไม่ได้รับอนุญาตสามารถเข้าถึงได้โดยไม่ตั้งใจ
    • บันทึกข้อผิดพลาดทั่วไปที่อาจพิมพ์ข้อมูลที่ละเอียดอ่อนในบางครั้ง ทั้งนี้ขึ้นอยู่กับข้อความแสดงข้อผิดพลาดที่ทริกเกอร์
  • บันทึกที่สร้างภายนอก:
    • คอมโพเนนต์ภายนอกมีหน้าที่พิมพ์บันทึกที่มีข้อมูลที่ละเอียดอ่อน

คำสั่ง Android Log.* จะเขียนไปยังบัฟเฟอร์หน่วยความจำทั่วไป logcat ตั้งแต่ Android 4.1 (ระดับ API 16) เป็นต้นมา เฉพาะแอปของระบบที่มีสิทธิ์เท่านั้นที่จะได้รับสิทธิ์เข้าถึงเพื่ออ่าน logcat โดยการประกาศสิทธิ์ READ_LOGS อย่างไรก็ตาม Android รองรับอุปกรณ์ที่หลากหลายอย่างยิ่ง ซึ่งบางครั้งแอปพลิเคชันที่โหลดไว้ล่วงหน้าจะประกาศสิทธิ์ READ_LOGS ด้วยเหตุนี้ เราจึงไม่แนะนำให้บันทึกโดยตรงไปยัง logcat เนื่องจากมีแนวโน้มที่จะเกิดการรั่วไหลของข้อมูลมากกว่า

ตรวจสอบว่าการบันทึกทั้งหมดไปยัง logcat ได้รับการล้างข้อมูลในแอปพลิเคชันเวอร์ชันที่ไม่ใช่การแก้ไขข้อบกพร่อง นำข้อมูลที่อาจเป็นข้อมูลที่ละเอียดอ่อนออก เพื่อเป็นการป้องกันเพิ่มเติม ให้ใช้เครื่องมืออย่างเช่น R8 เพื่อนำระดับบันทึกทั้งหมดออก ยกเว้นคำเตือนและข้อผิดพลาด หากต้องการบันทึกโดยละเอียดเพิ่มเติม ให้ใช้ที่จัดเก็บข้อมูลภายในและจัดการบันทึกของคุณเองโดยตรงแทนการใช้บันทึกของระบบ

ผลกระทบ

ความรุนแรงของช่องโหว่ในคลาสการเปิดเผยข้อมูลบันทึกอาจแตกต่างกันไป ขึ้นอยู่กับบริบทและประเภทของข้อมูลที่ละเอียดอ่อน โดยโดยรวมแล้ว ผลกระทบของช่องโหว่ในคลาสนี้คือการสูญเสียการรักษาความลับของข้อมูลที่อาจมีความสำคัญ เช่น PII และข้อมูลเข้าสู่ระบบ

การลดปัญหา

ทั่วไป

ในฐานะมาตรการป้องกันล่วงหน้าทั่วไประหว่างการออกแบบและการติดตั้งใช้งาน ให้กำหนดขอบเขตความน่าเชื่อถือตามหลักการให้สิทธิ์ขั้นต่ำที่สุด ในอุดมคติแล้ว ข้อมูลที่ละเอียดอ่อนไม่ควรข้ามหรือไปถึงภายนอกพื้นที่ที่เชื่อถือได้ ซึ่งจะช่วยเสริมการแยกสิทธิ์

อย่าบันทึกข้อมูลที่ละเอียดอ่อน บันทึกเฉพาะค่าคงที่ในเวลาคอมไพล์ทุกครั้งที่เป็นไปได้ คุณสามารถใช้เครื่องมือ ErrorProne สำหรับคำอธิบายประกอบค่าคงที่ขณะคอมไพล์

หลีกเลี่ยงบันทึกที่พิมพ์ข้อความที่อาจมีข้อมูลที่ไม่คาดคิด รวมถึงข้อมูลที่ละเอียดอ่อน ทั้งนี้ขึ้นอยู่กับข้อผิดพลาดที่ทริกเกอร์ ข้อมูลที่พิมพ์ในบันทึกและบันทึกข้อผิดพลาดควรมีเฉพาะข้อมูลที่คาดการณ์ได้มากที่สุด

หลีกเลี่ยงการบันทึกไปยัง logcat เนื่องจากการบันทึกไปยัง logcat อาจกลายเป็นปัญหาด้านความเป็นส่วนตัวเนื่องจากแอปที่มีสิทธิ์ READ_LOGS นอกจากนี้ยังไม่มีประสิทธิภาพเนื่องจากไม่สามารถทริกเกอร์การแจ้งเตือนหรือค้นหาได้ เราขอแนะนำให้แอปพลิเคชันกำหนดค่าlogcatแบ็กเอนด์สำหรับการสร้างของนักพัฒนาแอปเท่านั้น

ไลบรารีการจัดการบันทึกส่วนใหญ่จะอนุญาตให้กำหนดระดับบันทึก ซึ่งจะช่วยให้บันทึกข้อมูลจำนวนที่แตกต่างกันระหว่างบันทึกการแก้ไขข้อบกพร่องและบันทึกการใช้งานจริงได้ เปลี่ยนระดับบันทึกเพื่อให้แตกต่างจาก "debug" ทันทีที่การทดสอบผลิตภัณฑ์สิ้นสุดลง

นำระดับบันทึกออกจากเวอร์ชันที่ใช้งานจริงให้ได้มากที่สุด หากหลีกเลี่ยงการเก็บรักษาบันทึกในเวอร์ชันที่ใช้งานจริงไม่ได้ ให้นำตัวแปรที่ไม่คงที่ออกจากคำสั่งบันทึก สถานการณ์ต่อไปนี้อาจเกิดขึ้นได้

  • คุณนำบันทึกทั้งหมดออกจากเวอร์ชันที่ใช้งานจริงได้
  • คุณต้องเก็บบันทึกคำเตือนและข้อผิดพลาดในเวอร์ชันที่ใช้งานจริง

สำหรับทั้ง 2 กรณีนี้ ให้นำบันทึกออกโดยอัตโนมัติโดยใช้ไลบรารี เช่น R8 การพยายามนำบันทึกออกด้วยตนเองอาจทำให้เกิดข้อผิดพลาดได้ คุณสามารถตั้งค่า R8 ให้นำระดับบันทึกที่คุณต้องการเก็บไว้สำหรับการแก้ไขข้อบกพร่องออกได้อย่างปลอดภัย แต่จะนำออกในเวอร์ชันที่ใช้งานจริง ซึ่งเป็นส่วนหนึ่งของการเพิ่มประสิทธิภาพโค้ด

หากจะบันทึกในเวอร์ชันที่ใช้งานจริง ให้เตรียม Flag ที่คุณใช้เพื่อปิดการบันทึกแบบมีเงื่อนไขในกรณีที่เกิดเหตุการณ์ได้ การแจ้งเหตุการณ์ควรให้ความสำคัญกับความปลอดภัยของการติดตั้งใช้งาน ความเร็วและความง่ายในการติดตั้งใช้งาน ความละเอียดของการปกปิดข้อมูลในบันทึก การใช้งานหน่วยความจำ และค่าใช้จ่ายด้านประสิทธิภาพของการสแกนข้อความบันทึกทุกรายการ

ลบบันทึกไปยัง Logcat จากบิลด์เวอร์ชันที่ใช้งานจริงโดยใช้ R8

ใน Android Studio 3.4 หรือปลั๊กอิน Android Gradle 3.4.0 ขึ้นไป R8 เป็นคอมไพเลอร์เริ่มต้นสำหรับการเพิ่มประสิทธิภาพและการลดขนาดโค้ด แต่คุณต้องเปิดใช้ R8

R8 ได้เข้ามาแทนที่ ProGuard แล้ว แต่ไฟล์กฎในโฟลเดอร์รูทของโปรเจ็กต์ยังคงชื่อ proguard-rules.pro อยู่ ข้อมูลโค้ดต่อไปนี้แสดงตัวอย่างไฟล์ proguard-rules.pro ที่นำบันทึกทั้งหมดออกจากเวอร์ชันที่ใช้งานจริงยกเว้นคำเตือนและข้อผิดพลาด

-assumenosideeffects class android.util.Log {
    private static final String TAG = "MyTAG";
    public static boolean isLoggable(java.lang.String, int);
    public static int v(TAG, "My log as verbose");
    public static int d(TAG, "My log as debug");
    public static int i(TAG, "My log as information");
}

ไฟล์ proguard-rules.pro ตัวอย่างต่อไปนี้จะนำบันทึกทั้งหมดออกจากเวอร์ชันที่ใช้งานจริง

-assumenosideeffects class android.util.Log {
    private static final String TAG = "MyTAG";
    public static boolean isLoggable(java.lang.String, int);
    public static int v(TAG, "My log as verbose");
    public static int d(TAG, "My log as debug");
    public static int i(TAG, "My log as information");
    public static int w(TAG, "My log as warning");
    public static int e(TAG, "My log as error");
}

โปรดทราบว่า R8 มีความสามารถในการลดขนาดแอปและฟังก์ชันการลบบันทึก หากต้องการใช้ R8 เฉพาะฟังก์ชันการลบบันทึก ให้เพิ่มข้อมูลต่อไปนี้ลงในไฟล์ proguard-rules.pro

-dontwarn **
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose

-optimizations !code/simplification/arithmetic,!code/allocation/variable
-keep class **
-keepclassmembers class *{*;}
-keepattributes *

ล้างข้อมูลบันทึกที่อาจเกิดขึ้นในการผลิตซึ่งมีข้อมูลที่ละเอียดอ่อน

โปรดตรวจสอบว่าการบันทึกทั้งหมดไปยัง logcat ได้รับการล้างข้อมูลในแอปพลิเคชันเวอร์ชันที่ไม่ใช่การแก้ไขข้อบกพร่องเพื่อหลีกเลี่ยงการรั่วไหลของข้อมูลที่ละเอียดอ่อน นำข้อมูลที่อาจเป็นข้อมูลที่ละเอียดอ่อนออก

ตัวอย่าง

Kotlin

data class Credential<T>(val data: String) {
  /** Returns a redacted value to avoid accidental inclusion in logs. */
  override fun toString() = "Credential XX"
}

fun checkNoMatches(list: List<Any>) {
    if (!list.isEmpty()) {
          Log.e(TAG, "Expected empty list, but was %s", list)
    }
}

Java

public class Credential<T> {
  private T t;
  /** Returns a redacted value to avoid accidental inclusion in logs. */
  public String toString(){
         return "Credential XX";
  }
}

private void checkNoMatches(List<E> list) {
   if (!list.isEmpty()) {
          Log.e(TAG, "Expected empty list, but was %s", list);
   }
}

ปกปิดข้อมูลที่ละเอียดอ่อนในบันทึก

หากคุณต้องรวมข้อมูลที่ละเอียดอ่อนไว้ในบันทึก เราขอแนะนำให้ล้างข้อมูลบันทึกก่อนพิมพ์เพื่อนำข้อมูลที่ละเอียดอ่อนออกหรือทำให้ข้อมูลดังกล่าวอ่านไม่ออก โดยใช้วิธีใดวิธีหนึ่งต่อไปนี้

  • การแปลงเป็นโทเค็น หากจัดเก็บข้อมูลที่ละเอียดอ่อนไว้ในห้องนิรภัย เช่น ระบบการจัดการการเข้ารหัสที่สามารถอ้างอิงข้อมูลลับผ่านโทเค็น ให้บันทึกโทเค็นแทนข้อมูลที่ละเอียดอ่อน
  • การมาสก์ข้อมูล การมาสก์ข้อมูลเป็นกระบวนการทางเดียวที่ย้อนกลับไม่ได้ โดยจะสร้างข้อมูลเวอร์ชันหนึ่งซึ่งมีลักษณะคล้ายกับข้อมูลต้นฉบับ แต่ซ่อนข้อมูลที่ละเอียดอ่อนที่สุดซึ่งอยู่ในฟิลด์ ตัวอย่าง: แทนที่หมายเลขบัตรเครดิต 1234-5678-9012-3456 ด้วย XXXX-XXXX-XXXX-1313 ก่อนที่จะเผยแพร่แอปในเวอร์ชันที่ใช้งานจริง เราขอแนะนำให้คุณทําตามกระบวนการตรวจสอบความปลอดภัยเพื่อตรวจสอบการใช้การมาสก์ข้อมูล คำเตือน: อย่าใช้การมาสก์ข้อมูลในกรณีที่แม้จะเผยแพร่ข้อมูลที่ละเอียดอ่อนเพียงบางส่วนก็อาจส่งผลกระทบต่อความปลอดภัยอย่างมาก เช่น เมื่อจัดการรหัสผ่าน
  • การปกปิดข้อมูล การปกปิดข้อมูลบางส่วนคล้ายกับการมาสก์ แต่จะซ่อนข้อมูลทั้งหมดที่อยู่ในฟิลด์ ตัวอย่าง: แทนที่หมายเลขบัตรเครดิต 1234-5678-9012-3456 ด้วย XXXX-XXXX-XXXX-XXXX
  • การกรอง ใช้สตริงรูปแบบในไลบรารีบันทึกที่คุณเลือก หากยังไม่มี เพื่อช่วยในการแก้ไขค่าที่ไม่ใช่ค่าคงที่ในคำสั่งบันทึก

การพิมพ์บันทึกควรดำเนินการผ่านคอมโพเนนต์ "เครื่องมือล้างข้อมูลบันทึก" เท่านั้น ซึ่งจะช่วยให้มั่นใจได้ว่าบันทึกทั้งหมดจะได้รับการล้างข้อมูลก่อนพิมพ์ ดังที่แสดงในข้อมูลโค้ดต่อไปนี้

Kotlin

data class ToMask<T>(private val data: T) {
  // Prevents accidental logging when an error is encountered.
  override fun toString() = "XX"

  // Makes it more difficult for developers to invoke sensitive data
  // and facilitates sensitive data usage tracking.
  fun getDataToMask(): T = data
}

data class Person(
  val email: ToMask<String>,
  val username: String
)

fun main() {
    val person = Person(
        ToMask("name@gmail.com"), 
        "myname"
    )
    println(person)
    println(person.email.getDataToMask())
}

Java

public class ToMask<T> {
  // Prevents accidental logging when an error is encountered.
  public String toString(){
         return "XX";
  }

  // Makes it more difficult for developers to invoke sensitive data 
  // and facilitates sensitive data usage tracking.
  public T  getDataToMask() {
    return this;
  }
}

public class Person {
  private ToMask<String> email;
  private String username;

  public Person(ToMask<String> email, String username) {
    this.email = email;
    this.username = username;
  }
}

public static void main(String[] args) {
    Person person = new Person(
        ToMask("name@gmail.com"), 
        "myname"
    );
    System.out.println(person);
    System.out.println(person.email.getDataToMask());
}