Skip to content
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 3 additions & 2 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading