flawopen.com/path-traversal/Kotlin
Path Traversal in Kotlin (java.io.File, Android)
File(dir, name) keeps ../ segments for the file system to follow, a canonicalPath.startsWith() check accepts a sibling folder such as attachments-old, and Uri.getLastPathSegment() decodes %2F so a content:// URI can smuggle ../ into an Android ContentProvider. How each mistake exposes app-private files such as shared_prefs, and how a canonical-path check with a trailing separator, File(name).name and FileProvider stop it.
Imagine an Android apartment building where each app keeps private files in a locked room. An exported ContentProvider is the building's front desk: it should open only files inside the app's shared room. If the desk joins a caller's file label to that room without checking the final canonical location, ../ in the label means “go back through a door” and can point to another room. Check that the resolved path stays beneath the intended directory, or share fixed locations through Android FileProvider.
Core Concepts & Subsystem Terms
File(parent, child) keeps ..File(dir, "../shared_prefs/auth.xml")keeps the..segments and the file system resolves them on open. UnlikePath.resolve, an absolute child is appended rather than replacing the parent, so..is the attack here.canonicalPath string prefixfile.canonicalPath.startsWith(dir.canonicalPath)compares characters, so.../attachments-old/secret.txtpasses. AppendFile.separatorto the base before comparing, or comparePathobjects.Uri.getLastPathSegment() decodes %2FgetLastPathSegment()returns the decoded segment, socontent://com.example.files/..%2F..%2Fshared_prefs%2Fauth.xmlyields../../shared_prefs/auth.xml. Any other app that can call your provider can use it to read your private files.File(name).name for server-supplied names- A download's file name from a
Content-Dispositionheader or API response can contain../.File(serverFileName).namekeeps only the last component; a generated name is safer still. FileProvider instead of a custom providerandroidx.core.content.FileProvidermaps a fixed set of directories declared in XML tocontent://URIs and resolves names inside them. Keep itexported="false"and share single files withFLAG_GRANT_READ_URI_PERMISSIONinstead of writing your ownopenFile.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-22
Step-by-Step Attack Flow
An untrusted name reaches File
A file name from a server response, an intent extra, or uri.lastPathSegment inside ContentProvider.openFile is passed to File(dir, name).
The name climbs out of the folder
It contains ../, or %2F-encoded slashes that lastPathSegment decodes, or names a sibling folder with the same prefix. The benign test value ../../shared_prefs/auth.xml illustrates the read case.
The file system opens the resolved path
FileOutputStream or ParcelFileDescriptor.open hands the path to the kernel, which follows every .. inside the app's data directory.
App data leaks or is overwritten
Another app reads session tokens from shared_prefs or a database file, or a malicious download overwrites a cached library or config the app loads later.
Source Code: Flaw vs. Secure Implementation
// AttachmentStore.kt: each method lets an untrusted name leave the attachments folder
package com.example.files
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import java.io.File
import java.io.InputStream
class AttachmentStore(context: Context) {
private val dir = File(context.filesDir, "attachments")
// a server name such as ../shared_prefs/auth.xml overwrites app data
fun save(serverFileName: String, body: InputStream): File {
val target = File(dir, serverFileName)
target.outputStream().use { body.copyTo(it) }
return target
}
// startsWith also accepts attachments-old
fun open(name: String): File {
val file = File(dir, name)
require(file.canonicalPath.startsWith(dir.canonicalPath)) { "outside attachments" }
return file
}
// lastPathSegment decodes %2F, so ../ reaches File
fun openForProvider(uri: Uri): ParcelFileDescriptor {
val file = File(dir, uri.lastPathSegment ?: "")
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
}
}
// AttachmentStore.kt: canonical path plus separator, reduced names
package com.example.files
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
class AttachmentStore(context: Context) {
private val dir = File(context.filesDir, "attachments").canonicalFile
// the separator stops sibling folders; canonicalFile removes .. and links
private fun inside(name: String): File {
val file = File(dir, name).canonicalFile
require(file.path.startsWith(dir.path + File.separator)) { "outside attachments" }
return file
}
// only the last component of the server name is used
fun save(serverFileName: String, body: InputStream): File {
val target = inside(File(serverFileName).name)
target.outputStream().use { body.copyTo(it) }
return target
}
// every lookup goes through the same containment check
fun open(name: String): File = inside(name)
// the decoded segment is checked before it is opened
fun openForProvider(uri: Uri): ParcelFileDescriptor {
val name = uri.lastPathSegment ?: throw FileNotFoundException("no file name")
return ParcelFileDescriptor.open(inside(name), ParcelFileDescriptor.MODE_READ_ONLY)
}
}
Engineering & System Hardening Checklist
- ✓Run CodeQL
java/path-injection(it covers Kotlin) in CI and flag everylastPathSegment, intent extra or header value passed toFile. - ✓Canonicalize with
canonicalFileand requirestartsWith(base.path + File.separator); never compare canonical paths without the separator. - ✓Reduce server-supplied names with
File(name).name, or store downloads under generated names. - ✓Share files through
FileProviderwithexported="false"and per-URI grants instead of a customopenFile. - ✓Keep sensitive data out of directories that any provider serves, and review every exported component in the merged manifest.