หมวดหมู่ 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());
}