From fe0285d3da01c3057a6f84cf670e2557a688cb52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 13:52:18 +0900 Subject: [PATCH 1/3] test(security): reproduce unicode hidden-file bypass --- .../html4tree/HiddenFileSecurityTest.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/test/kotlin/html4tree/HiddenFileSecurityTest.kt diff --git a/src/test/kotlin/html4tree/HiddenFileSecurityTest.kt b/src/test/kotlin/html4tree/HiddenFileSecurityTest.kt new file mode 100644 index 0000000..abac4a2 --- /dev/null +++ b/src/test/kotlin/html4tree/HiddenFileSecurityTest.kt @@ -0,0 +1,24 @@ +package html4tree + +import org.junit.Test +import java.nio.file.Files +import kotlin.test.assertTrue + +class HiddenFileSecurityTest { + @Test + fun unicodeDotHomoglyphsAreExcludedFromDirectoryIndexes() { + val directory = Files.createTempDirectory("html4tree-homoglyph-").toFile() + try { + val hiddenNames = listOf("\u3002env", "\uFF0Egit", "\uFF61ssh") + hiddenNames.forEach { name -> directory.resolve(name).writeText("secret") } + + val excluded = process_ignore_file(directory) + + hiddenNames.forEach { name -> + assertTrue(name in excluded, "Unicode dot homoglyph must be treated as hidden: $name") + } + } finally { + directory.deleteRecursively() + } + } +} From bfd5f1816223b5ce8168a7cab1b55252c1dd5a3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 14:01:30 +0900 Subject: [PATCH 2/3] test(security): cover hidden-file classifier boundaries --- src/test/kotlin/html4tree/HiddenFileSecurityTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/kotlin/html4tree/HiddenFileSecurityTest.kt b/src/test/kotlin/html4tree/HiddenFileSecurityTest.kt index abac4a2..3bee2a1 100644 --- a/src/test/kotlin/html4tree/HiddenFileSecurityTest.kt +++ b/src/test/kotlin/html4tree/HiddenFileSecurityTest.kt @@ -2,9 +2,21 @@ package html4tree import org.junit.Test import java.nio.file.Files +import kotlin.test.assertFalse import kotlin.test.assertTrue class HiddenFileSecurityTest { + @Test + fun hiddenFileClassifierRecognizesAsciiAndUnicodeDotPrefixes() { + val hiddenNames = listOf(".env", "\u3002env", "\uFF0Egit", "\uFF61ssh") + + hiddenNames.forEach { name -> + assertTrue(name.isHiddenFile(), "Dot-like prefix must be treated as hidden: $name") + } + assertFalse("visible.txt".isHiddenFile()) + assertFalse("".isHiddenFile()) + } + @Test fun unicodeDotHomoglyphsAreExcludedFromDirectoryIndexes() { val directory = Files.createTempDirectory("html4tree-homoglyph-").toFile() From 847c85b8780e5f4e4b996e8f45d6df3e6e8690c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 14:02:33 +0900 Subject: [PATCH 3/3] fix(security): block Unicode dot hidden-file homoglyphs --- src/main/kotlin/html4tree/main.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index ff480cc..8942c04 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -182,7 +182,7 @@ internal fun crawl_directories( dirFiles?.forEach { // ⚡ Bolt Performance Optimization: Short-circuit OS stat calls // by checking cheap in-memory string exclusion rules first - if(!it.name.startsWith(".") && it.name !in exclude) { + if(!it.name.isHiddenFile() && it.name !in exclude) { val childAttrs = readAttributes(it) if(childAttrs != null && childAttrs.isDirectory && !childAttrs.isSymbolicLink) { val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key) @@ -195,6 +195,13 @@ internal fun crawl_directories( } } +fun String.isHiddenFile(): Boolean { + return when (firstOrNull()) { + '.', '\u3002', '\uFF0E', '\uFF61' -> true + else -> false + } +} + // ⚡ Bolt Performance Optimization: Single-pass loop with lazy StringBuilder // Chained `.replace()` calls allocate multiple intermediate strings. // A single pass over the string lazily allocating a StringBuilder is much faster. @@ -310,9 +317,9 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S val defaultSensitiveFiles = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json") files_to_exclude.addAll(defaultSensitiveFiles) - // 보안 향상: .env, .git 등 민감한 정보가 포함될 수 있는 숨김 파일(.으로 시작하는 모든 항목)을 기본적으로 노출하지 않도록 제외 (정보 노출 방지) + // 보안 향상: dot-like prefixes are treated as hidden to prevent visually-confusable sensitive entries from reaching generated indexes. (dirFilesNames ?: curr_dir.list())?.forEach { - if (it.startsWith(".")) { + if (it.isHiddenFile()) { files_to_exclude.add(it) } } @@ -366,7 +373,7 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array val fileName = it.getName() // ⚡ Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls // 🛡️ Sentinel: Ignore hidden files/directories to prevent sensitive data exposure - if (!fileName.startsWith(".") && fileName !in exclude) { + if (!fileName.isHiddenFile() && fileName !in exclude) { var isLinkedDirectory = false var isSymbolicLink = false try {