From a5f026065b023bb1fa6b5b1b62e43da2e585b20c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:24:20 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20?= =?UTF-8?q?=EC=88=A8=EA=B2=A8=EC=A7=84=20=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EB=AF=BC?= =?UTF-8?q?=EA=B0=90=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 4 ++++ src/main/kotlin/html4tree/main.kt | 5 +++-- src/test/kotlin/html4tree/MainTest.kt | 27 +++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) 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") From 8f99c58f8fc2cb01e2bd31d04d058ef91b2be3ec Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:25:02 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20?= =?UTF-8?q?=EC=88=A8=EA=B2=A8=EC=A7=84=20=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EB=AF=BC?= =?UTF-8?q?=EA=B0=90=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From da1ff9c71a50c2a7da29921f6ff42aaf5cae4281 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:32:26 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20?= =?UTF-8?q?=EC=88=A8=EA=B2=A8=EC=A7=84=20=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EB=AF=BC?= =?UTF-8?q?=EA=B0=90=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit