ContentProvider가 제공한 파일 이름을 잘못 신뢰하는 경우

OWASP 카테고리: MASVS-CODE: 코드 품질

개요

ContentProvider의 서브클래스인 FileProvider의 용도는 애플리케이션('서버 애플리케이션')이 다른 애플리케이션('클라이언트 애플리케이션')과 파일을 공유할 안전한 방법을 제공하는 것입니다. 그러나 클라이언트 애플리케이션이 서버 애플리케이션에서 제공한 파일 이름을 올바르게 처리하지 못하면 공격자가 제어하는 서버 애플리케이션이 자체적인 악성 FileProvider를 구현하여 클라이언트 애플리케이션의 앱 전용 스토리지에 있는 파일을 덮어쓸 수 있게 됩니다

영향

공격자가 애플리케이션의 파일을 덮어쓸 수 있으면 (애플리케이션의 코드를 덮어쓰는 방법으로) 악성 코드가 실행되거나 (애플리케이션의 공유 환경설정 또는 기타 구성 파일을 덮어쓰는 방법으로) 애플리케이션 동작이 수정될 수 있습니다.

완화 조치

사용자 입력을 신뢰하지 말 것

파일 시스템 호출을 사용할 때는 고유한 파일 이름을 생성하여 수신된 파일을 스토리지에 쓰는 방법을 통해 사용자 입력을 사용하지 않습니다.

즉, 클라이언트 애플리케이션이 수신된 파일을 스토리지에 쓸 때는 서버 애플리케이션이 제공한 파일 이름은 무시하고 내부에서 자체적으로 생성한 고유 식별자를 파일 이름으로 사용해야 합니다.

다음은 https://developer.android.com/training/secure-file-sharing/request-file에 있는 코드를 기반으로 하는 예시입니다.

Kotlin

// Code in
// https://developer.android.com/training/secure-file-sharing/request-file#OpenFile
// used to obtain file descriptor (fd)

try {
    val inputStream = FileInputStream(fd)
    val tempFile = File.createTempFile("temp", null, cacheDir)
    val outputStream = FileOutputStream(tempFile)
    val buf = ByteArray(1024)
    var len: Int
    len = inputStream.read(buf)
    while (len > 0) {
        if (len != -1) {
            outputStream.write(buf, 0, len)
            len = inputStream.read(buf)
        }
    }
    inputStream.close()
    outputStream.close()
} catch (e: IOException) {
    e.printStackTrace()
    Log.e("MainActivity", "File copy error.")
    return
}

Java

// Code in
// https://developer.android.com/training/secure-file-sharing/request-file#OpenFile
// used to obtain file descriptor (fd)

FileInputStream inputStream = new FileInputStream(fd);

// Create a temporary file
File tempFile = File.createTempFile("temp", null, getCacheDir());

// Copy the contents of the file to the temporary file
try {
    OutputStream outputStream = new FileOutputStream(tempFile))
    byte[] buffer = new byte[1024];
    int length;
    while ((length = inputStream.read(buffer)) > 0) {
        outputStream.write(buffer, 0, length);
    }
} catch (IOException e) {
    e.printStackTrace();
    Log.e("MainActivity", "File copy error.");
    return;
}

제공된 파일 이름 정리하기

수신된 파일을 스토리지에 쓸 때는 제공된 파일 이름을 정리합니다.

이 완화 조치는 모든 잠재적인 사례에 대응하기 어려울 수 있기 때문에 앞에 나온 완화 조치에 비해 바람직하지 않습니다. 단, 고유한 파일 이름을 생성하기가 어렵다면 클라이언트 애플리케이션이 제공된 파일 이름을 정리해야 합니다. 여기서 정리란 다음과 같은 작업을 포함합니다.

  • 파일 이름에서 경로 순회 문자 삭제
  • 경로 순회가 없도록 하기 위해 표준화 실행

다음은 파일 정보 가져오기에 나온 지침을 기반으로 하는 예시입니다.

Kotlin

protected fun sanitizeFilename(displayName: String): String {
    val badCharacters = arrayOf("..", "/")
    val segments = displayName.split("/")
    var fileName = segments[segments.size - 1]
    for (suspString in badCharacters) {
        fileName = fileName.replace(suspString, "_")
    }
    return fileName
}

val displayName = returnCursor.getString(nameIndex)
val fileName = sanitizeFilename(displayName)
val filePath = File(context.filesDir, fileName).path

// saferOpenFile defined in Android developer documentation
val outputFile = saferOpenFile(filePath, context.filesDir.canonicalPath)

// fd obtained using Requesting a shared file from Android developer
// documentation

val inputStream = FileInputStream(fd)

// Copy the contents of the file to the new file
try {
    val outputStream = FileOutputStream(outputFile)
    val buffer = ByteArray(1024)
    var length: Int
    while (inputStream.read(buffer).also { length = it } > 0) {
        outputStream.write(buffer, 0, length)
    }
} catch (e: IOException) {
    // Handle exception
}

Java

protected String sanitizeFilename(String displayName) {
    String[] badCharacters = new String[] { "..", "/" };
    String[] segments = displayName.split("/");
    String fileName = segments[segments.length - 1];
    for (String suspString : badCharacters) {
        fileName = fileName.replace(suspString, "_");
    }
    return fileName;
}

String displayName = returnCursor.getString(nameIndex);
String fileName = sanitizeFilename(displayName);
String filePath = new File(context.getFilesDir(), fileName).getPath();

// saferOpenFile defined in Android developer documentation

File outputFile = saferOpenFile(filePath,
    context.getFilesDir().getCanonicalPath());

// fd obtained using Requesting a shared file from Android developer
// documentation

FileInputStream inputStream = new FileInputStream(fd);

// Copy the contents of the file to the new file
try {
    OutputStream outputStream = new FileOutputStream(outputFile))
    byte[] buffer = new byte[1024];
    int length;
    while ((length = inputStream.read(buffer)) > 0) {
        outputStream.write(buffer, 0, length);
    }
} catch (IOException e) {
    // Handle exception
}

도움을 주신 분들: Microsoft Threat Intelligence의 Dimitrios Valsamaras 및 Michael Peck

리소스