diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 012c3f5..fc2f66a 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -27,3 +27,7 @@ **Vulnerability:** `File(path).canonicalFile`를 사용하여 심볼릭 링크 여부를 검사하면, `canonicalFile` 함수 내부에서 심볼릭 링크를 이미 대상(target) 파일의 실제 경로로 해석(resolve)해 버리기 때문에, 이후에 진행되는 `Files.isDirectory(..., LinkOption.NOFOLLOW_LINKS)` 등의 심볼릭 링크 제한 검사가 완전히 무력화되는 취약점이 발견되었습니다. **Learning:** `canonicalFile`은 보안 검사(경로 조작 등)를 위해 절대 경로를 얻을 때 유용할 수 있지만, 심볼릭 링크 자체의 특성(심볼릭 링크인지 아닌지)을 보존해야 하는 맥락에서는 사용하면 안 됩니다. **Prevention:** 심볼릭 링크 여부를 검사해야 하거나 심볼릭 링크 자체를 제한해야 하는 경우에는 `canonicalFile` 대신 `absoluteFile.toPath().normalize().toFile()`과 같이 심볼릭 링크를 해석하지 않고 경로만 정규화하는 방식을 사용해야 합니다. +## 2024-07-07 - [Sensitive Data Exposure in Directory Indexing] +**Vulnerability:** The application was traversing and listing hidden files and directories (those starting with `.`), potentially exposing sensitive information like `.git` histories or `.env` configuration files in the generated HTML index. +**Learning:** This existed because the traversal and filtering logic did not explicitly account for standard conventions regarding hidden files, defaulting to listing everything not explicitly ignored. +**Prevention:** Always implement explicit filters for hidden files and directories (e.g., `!file.name.startsWith(".")`) in applications that generate static files or expose directory structures to the public. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 1710d99..7f550b5 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -42,7 +42,7 @@ fun go(topDir: String, maxLevel: Int) { if(maxLevel == -1 || currentLevel < maxLevel) { lle.file.listFiles()?.forEach { - if(Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS)){ + if(!it.name.startsWith(".") && Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS)){ ll.push( LinkedListEntry(it, currentLevel+1)) } } @@ -201,7 +201,8 @@ fun process_dir(curr_dir: File){ dir_files.sortWith(compareBy ({it.name}) ) dir_files.forEach { val isLinkedDirectory = Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) - if((it.getName() !in exclude) && (isLinkedDirectory || !it.isDirectory()) && !Files.isSymbolicLink(it.toPath())) { + // 🛡️ Sentinel: Ignore hidden files/directories to prevent sensitive data exposure + if(!it.name.startsWith(".") && (it.getName() !in exclude) && (isLinkedDirectory || !it.isDirectory()) && !Files.isSymbolicLink(it.toPath())) { val fileName = it.getName() val encodedHref = if (isLinkedDirectory) { "./${fileName.urlEncodePath()}/" } else { "./${fileName.urlEncodePath()}" } val ariaLabel = "${fileName} ${if (isLinkedDirectory) { "디렉토리" } else { "파일" }}".escapeHtml() diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index a3f8c76..f9fe95d 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -96,6 +96,33 @@ class MainTest { assertTrue(htmlContent.contains("이 디렉토리는 비어 있습니다.")) } + @Test + fun testGoIgnoresHiddenFilesAndDirectories() { + val hiddenFile = File(tempDir, ".hidden_file.txt") + hiddenFile.createNewFile() + + val hiddenDir = File(tempDir, ".hidden_dir") + hiddenDir.mkdir() + val fileInHiddenDir = File(hiddenDir, "file_in_hidden_dir.txt") + fileInHiddenDir.createNewFile() + + val normalFile = File(tempDir, "normal_file.txt") + normalFile.createNewFile() + + go(tempDir.absolutePath, -1) + + val indexFile = File(tempDir, "index.html") + assertTrue(indexFile.exists()) + val htmlContent = indexFile.readText() + + assertTrue(htmlContent.contains("normal_file.txt"), "normal_file.txt should be listed") + assertFalse(htmlContent.contains(".hidden_file.txt"), ".hidden_file.txt should not be listed") + assertFalse(htmlContent.contains(".hidden_dir"), ".hidden_dir should not be listed") + + val hiddenDirIndexFile = File(hiddenDir, "index.html") + assertFalse(hiddenDirIndexFile.exists(), "Hidden directories should not be traversed to generate index.html") + } + @Test fun testProcessIgnoreFile() { val ignoreFile = File(tempDir, ".html4ignore")