-
Notifications
You must be signed in to change notification settings - Fork 2
🔒 [security fix] Fix Path Traversal in Document Provider (Updated) #215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,11 +85,23 @@ class YuhaiinDocumentProvider : DocumentsProvider() { | |
| } | ||
|
|
||
| override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean { | ||
| if (documentId != null) { | ||
| return parentDocumentId?.let { documentId.startsWith(it) } ?: false | ||
| if (parentDocumentId == null || documentId == null) { | ||
| return false | ||
| } | ||
| return isChild(File(parentDocumentId), File(documentId)) | ||
| } | ||
|
|
||
| return false | ||
| internal fun isChild(parent: File, child: File): Boolean { | ||
| return try { | ||
| val canonicalParent = parent.canonicalPath | ||
| val canonicalChild = child.canonicalPath | ||
| if (canonicalChild == canonicalParent) return true | ||
| val parentPathWithSeparator = | ||
| if (canonicalParent.endsWith(File.separator)) canonicalParent else canonicalParent + File.separator | ||
| canonicalChild.startsWith(parentPathWithSeparator) | ||
| } catch (e: IOException) { | ||
| false | ||
| } | ||
| } | ||
|
|
||
| override fun querySearchDocuments( | ||
|
|
@@ -119,12 +131,7 @@ class YuhaiinDocumentProvider : DocumentsProvider() { | |
| val file = pending.removeAt(0) | ||
| // Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search | ||
| // through the whole SD card). | ||
| val isInsideHome: Boolean = try { | ||
| file.canonicalPath.startsWith(baseDir.toString()) | ||
| } catch (_: IOException) { | ||
| true | ||
| } | ||
| if (isInsideHome) { | ||
| if (isChild(baseDir, file)) { | ||
| if (file.isDirectory) { | ||
| file.listFiles()?.let { pending.addAll(it) } | ||
| } else { | ||
|
|
@@ -144,6 +151,7 @@ class YuhaiinDocumentProvider : DocumentsProvider() { | |
| private fun getFileForDocId(docId: String): File { | ||
| val f = File(docId) | ||
| if (!f.exists()) throw FileNotFoundException(f.absolutePath + " not found") | ||
| if (!isChild(baseDir, f)) throw FileNotFoundException("Invalid document ID: $docId") | ||
|
Comment on lines
153
to
+154
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The if (!isChild(baseDir, f)) throw FileNotFoundException("Invalid document ID: $docId")
if (!f.exists()) throw FileNotFoundException(f.absolutePath + " not found") |
||
| return f | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package io.github.asutorufa.yuhaiin.docuemntprovider | ||
|
|
||
| import org.junit.Assert.assertFalse | ||
| import org.junit.Assert.assertTrue | ||
| import org.junit.Test | ||
| import java.io.File | ||
| import java.nio.file.Files | ||
|
|
||
| class PathTraversalTest { | ||
|
|
||
| @Test | ||
| fun testIsChild() { | ||
| val provider = YuhaiinDocumentProvider() | ||
| val tempDir = Files.createTempDirectory("yuhaiin_test").toFile().canonicalFile | ||
| val otherDir = Files.createTempDirectory("yuhaiin_other").toFile().canonicalFile | ||
| val partialMatchDir = File(tempDir.parentFile, tempDir.name + "_extra") | ||
| try { | ||
| val subDir = File(tempDir, "subdir") | ||
| subDir.mkdir() | ||
| val fileInSubDir = File(subDir, "file.txt") | ||
| fileInSubDir.createNewFile() | ||
|
|
||
| val otherFile = File(otherDir, "other.txt") | ||
| otherFile.createNewFile() | ||
|
|
||
| // Construct a path that looks like it's inside tempDir but isn't after canonicalization | ||
| val traversalFile = File(tempDir, "../" + otherDir.name + "/other.txt") | ||
|
|
||
| assertTrue("Should be child of itself", provider.isChild(tempDir, tempDir)) | ||
| assertTrue("Should be child of tempDir", provider.isChild(tempDir, subDir)) | ||
| assertTrue("Should be child of tempDir", provider.isChild(tempDir, fileInSubDir)) | ||
|
|
||
| assertFalse("Should not be child of otherDir", provider.isChild(tempDir, otherDir)) | ||
| assertFalse("Should not be child of otherDir", provider.isChild(tempDir, otherFile)) | ||
| assertFalse("Traversal should be blocked: ${traversalFile.path}", provider.isChild(tempDir, traversalFile)) | ||
|
|
||
| partialMatchDir.mkdir() | ||
| assertFalse("Partial name match should be blocked: ${partialMatchDir.path}", provider.isChild(tempDir, partialMatchDir)) | ||
| } finally { | ||
| tempDir.deleteRecursively() | ||
| otherDir.deleteRecursively() | ||
| partialMatchDir.deleteRecursively() | ||
| } | ||
| } | ||
|
Comment on lines
+11
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current test implementation for resource cleanup has a potential resource leak. If the creation of Please add the following imports at the top of the file: import org.junit.Rule
import org.junit.rules.TemporaryFolder @get:Rule
val tempFolder = TemporaryFolder()
@Test
fun testIsChild() {
val provider = YuhaiinDocumentProvider()
val tempDir = tempFolder.newFolder("yuhaiin_test").canonicalFile
val otherDir = tempFolder.newFolder("yuhaiin_other").canonicalFile
val partialMatchDir = File(tempDir.parentFile, tempDir.name + "_extra")
val subDir = File(tempDir, "subdir")
subDir.mkdir()
val fileInSubDir = File(subDir, "file.txt")
fileInSubDir.createNewFile()
val otherFile = File(otherDir, "other.txt")
otherFile.createNewFile()
// Construct a path that looks like it's inside tempDir but isn't after canonicalization
val traversalFile = File(tempDir, "../" + otherDir.name + "/other.txt")
assertTrue("Should be child of itself", provider.isChild(tempDir, tempDir))
assertTrue("Should be child of tempDir", provider.isChild(tempDir, subDir))
assertTrue("Should be child of tempDir", provider.isChild(tempDir, fileInSubDir))
assertFalse("Should not be child of otherDir", provider.isChild(tempDir, otherDir))
assertFalse("Should not be child of otherDir", provider.isChild(tempDir, otherFile))
assertFalse("Traversal should be blocked: ${traversalFile.path}", provider.isChild(tempDir, traversalFile))
partialMatchDir.mkdir()
assertFalse("Partial name match should be blocked: ${partialMatchDir.path}", provider.isChild(tempDir, partialMatchDir))
} |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
isChildDocumentmethod verifies the parent-child relationship between any two paths on the filesystem without ensuring that they belong to the provider's authorized directory (baseDir). This allows an attacker to probe the filesystem structure and verify the existence of files and directories outside the provider's scope by checking their relationships. To fix this, ensure that theparentDocumentIdis validated to be withinbaseDirbefore performing the relationship check.