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

หมวดหมู่ OWASP: MASVS-STORAGE: พื้นที่เก็บข้อมูล

ภาพรวม

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

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

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

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

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

ผลกระทบ

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

การลดปัญหา

ทั่วไป

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

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

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

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

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

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

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

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

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

ลบบันทึกไปยังบันทึกจากบิลด์เวอร์ชันที่ใช้งานจริงโดยใช้ 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());
}