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