diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a21dad7..fe7aecc 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -45,3 +45,7 @@ **Vulnerability:** The `.html4ignore` parser still allowed excessively large files and root-directory crawls could generate unbounded filesystem output. **Learning:** Local CLI configuration inputs and traversal roots need explicit resource ceilings, not only syntactic validation. **Prevention:** Limit `.html4ignore` file size, parsed line count, compiled pattern count, and regex length; reject filesystem root traversal using `File.parentFile != null`. +## 2024-07-11 - [.html4ignore 파일의 ReDoS 취약점 수정 (Glob 패턴 적용)] +**Vulnerability:** [사용자가 제공한 `.html4ignore` 패턴을 직접 정규표현식으로 컴파일하여 발생하는 ReDoS(정규표현식 서비스 거부) 취약점 발견.] +**Learning:** [필터링 패턴으로 정규표현식을 직접 노출하면 악의적으로 조작된 긴 문자열이나 복잡한 패턴을 통해 애플리케이션의 리소스를 고갈시킬 수 있음.] +**Prevention:** [사용자 입력 패턴은 정규표현식으로 변환하기 전 `java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$pattern")`와 같은 안전한 Glob 매칭 방식을 사용해야 함.] diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 936c2b7..c2a7f43 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -122,7 +122,7 @@ fun process_ignore_file(curr_dir: File): Set { // 보안 향상: .html4ignore 파일이 일반 파일인지 확인하고, 심볼릭 링크인 경우 무시하여 DoS 및 경로 조작을 방지합니다. // 보안 향상: 파일 크기(1MB 제한) 및 줄 수(1000줄), 정규식 길이(100자)를 제한하여 ReDoS 및 메모리 고갈(OOM) 방지 if(ignore_file.isFile && !Files.isSymbolicLink(ignore_file.toPath()) && ignore_file.length() <= 1048576){ - val ignored_regexes = mutableListOf() + val ignored_matchers = mutableListOf() ignore_file.useLines { lines -> for ((lineIndex, it) in lines.withIndex()) { @@ -131,8 +131,8 @@ fun process_ignore_file(curr_dir: File): Set { val pattern = it.trim() if (pattern.isNotEmpty() && pattern.length <= 100) { try { - ignored_regexes.add(("^"+pattern+"$").toRegex()) - } catch (_: IllegalArgumentException) { + ignored_matchers.add(java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$pattern")) + } catch (_: java.util.regex.PatternSyntaxException) { } } } @@ -140,8 +140,9 @@ fun process_ignore_file(curr_dir: File): Set { curr_dir.list()?.sorted()?.forEach { val current = it - ignored_regexes.forEach { regex -> - if(regex.matches(current)){ + val pathCurrent = java.nio.file.Paths.get(current) + ignored_matchers.forEach { matcher -> + if(matcher.matches(pathCurrent)){ files_to_exclude.add(current) } } diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 9988cee..024e2e0 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -99,7 +99,7 @@ class MainTest { @Test fun testProcessIgnoreFile() { val ignoreFile = File(tempDir, ".html4ignore") - ignoreFile.writeText(".*\\.txt\n.*\\.log") + ignoreFile.writeText("*.txt\n*.log") File(tempDir, "test.txt").createNewFile() File(tempDir, "test.log").createNewFile() @@ -123,7 +123,7 @@ class MainTest { @Test fun testProcessIgnoreFileInvalidRegex() { val ignoreFile = File(tempDir, ".html4ignore") - ignoreFile.writeText("[\n.*\\.log") + ignoreFile.writeText("[\n*.log") File(tempDir, "test.log").createNewFile() File(tempDir, "test.txt").createNewFile() @@ -140,7 +140,7 @@ class MainTest { subdir.mkdir() File(tempDir, "file1.txt").createNewFile() File(tempDir, "test.ignore").createNewFile() - File(tempDir, ".html4ignore").writeText(".*\\.ignore") + File(tempDir, ".html4ignore").writeText("*.ignore") process_dir(tempDir) @@ -302,7 +302,7 @@ class MainTest { @Test fun testProcessIgnoreFileWithIndexHtml() { val ignoreFile = File(tempDir, ".html4ignore") - ignoreFile.writeText("index\\.html") + ignoreFile.writeText("index.html") File(tempDir, "index.html").writeText("existing") val excluded = process_ignore_file(tempDir) assertTrue(excluded.contains("index.html")) @@ -340,7 +340,7 @@ class MainTest { @Test fun testProcessIgnoreFileEmptyLine() { val ignoreFile = File(tempDir, ".html4ignore") - ignoreFile.writeText("\n.*\\.txt\n\n.*\\.log\n") + ignoreFile.writeText("\n*.txt\n\n*.log\n") File(tempDir, "test.txt").createNewFile() @@ -387,7 +387,7 @@ class MainTest { @Test fun testIgnoreFileIsSymlink() { val targetFile = File(tempDir, "target.ignore") - targetFile.writeText(".*\\.txt") + targetFile.writeText("*.txt") val ignoreFile = File(tempDir, ".html4ignore") try { Files.createSymbolicLink(ignoreFile.toPath(), targetFile.toPath()) @@ -421,8 +421,8 @@ class MainTest { @Test fun testProcessIgnoreFileLongRegex() { val ignoreFile = File(tempDir, ".html4ignore") - val longRegex = ".*".repeat(55) // Length 110 - ignoreFile.writeText("$longRegex\n.*\\.log") + val longRegex = "*".repeat(110) // Length 110 + ignoreFile.writeText("$longRegex\n*.log") File(tempDir, "test.log").createNewFile() File(tempDir, "test.txt").createNewFile() @@ -440,7 +440,7 @@ class MainTest { val ignoreFile = File(tempDir, ".html4ignore") val content = StringBuilder() for (i in 1..1005) { - content.append(".*\\.txt$i\n") + content.append("*.txt$i\n") } ignoreFile.writeText(content.toString())