diff --git a/.jules/sentinel.md b/.jules/sentinel.md index cdf88010..cfce3f83 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -88,3 +88,8 @@ **Vulnerability:** CSP 해시 불일치로 인한 인라인 스타일 차단 **Learning:** 브라우저는 인라인 스크립트와 스타일의 내부 텍스트(공백과 줄바꿈 포함)를 정확하게 해싱하여 Content-Security-Policy(CSP) 해시와 비교합니다. Kotlin의 멀티라인 문자열(`"""`)을 사용하여 템플릿에 콘텐츠를 주입할 때 암묵적인 여백이나 줄바꿈이 추가되면 최종 HTML 문자열이 변경되어 CSP 해시가 무효화됩니다. **Prevention:** 콘텐츠를 해싱하기 전에 `.trimIndent()`를 적용하여 원본 문자열을 정규화하고, HTML 템플릿에 주입할 때 ``와 같이 공백 없이 주입하여 해시가 완벽하게 일치하도록 해야 합니다. + +## 2024-08-08 - [html4tree] 유니코드 호모글리프(Homoglyph)를 이용한 숨김 파일 필터링 우회 방지 +**Vulnerability:** 유니코드의 점(dot) 변형 문자(예: U+3002, U+FF0E, U+FF61)를 사용하여 숨김 파일 검사(startsWith("."))를 우회하고, 민감한 디렉토리나 파일을 인덱스에 노출시키는 보안 필터 우회(Bypass) 취약점. +**Learning:** 숨김 파일을 걸러내기 위해 단순하게 점(.)으로 시작하는지만 검사할 경우, 공격자가 점과 시각적 및 의미적으로 유사한 유니코드 호모글리프 문자를 사용하여 검사를 우회할 수 있습니다. +**Prevention:** 파일 이름 기반의 필터링을 구현할 때, 일반적인 ASCII 점(.) 뿐만 아니라 악용 가능한 유니코드 호모글리프 점(dot) 변형 문자들도 함께 검사하는 강력한 필터링 함수(예: isHiddenFile)를 사용하여 우회를 방지하십시오. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index f52a1468..3b87d98c 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -13,6 +13,12 @@ import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.types.int +fun String.isHiddenFile(): Boolean { + if (this.isEmpty()) return false + val firstChar = this[0] + return firstChar == '.' || firstChar == '\u3002' || firstChar == '\uFF0E' || firstChar == '\uFF61' +} + private val CSS_CONTENT = """ body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; @@ -176,7 +182,7 @@ internal fun crawl_directories( dirFiles?.forEach { // ⚡ Bolt Performance Optimization: Short-circuit OS stat calls (isDirectory/isSymbolicLink) // by checking cheap in-memory string exclusion rules first - if(!it.name.startsWith(".") && it.name !in exclude && isDirectory(it) && !isSymbolicLink(it)) { + if(!it.name.isHiddenFile() && it.name !in exclude && isDirectory(it) && !isSymbolicLink(it)) { val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key) ll.push(childEntry) } @@ -303,7 +309,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S // 보안 향상: .env, .git 등 민감한 정보가 포함될 수 있는 숨김 파일(.으로 시작하는 모든 항목)을 기본적으로 노출하지 않도록 제외 (정보 노출 방지) (dirFilesNames ?: curr_dir.list())?.forEach { - if (it.startsWith(".")) { + if (it.isHiddenFile()) { files_to_exclude.add(it) } } @@ -357,7 +363,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 { diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 83739c9c..c63a901d 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -706,4 +706,9 @@ class MainTest { assertFalse(processed, "fileKey mismatch should skip directory processing") assertFalse(listed, "fileKey mismatch should skip child listing") } + + @Test + fun testIsHiddenFileEmptyString() { + assertFalse("".isHiddenFile()) + } }