diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a885865d..b8f034d4 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -99,3 +99,8 @@ **Root cause:** The protected implementation added canonical names to the exclusion set but did not compare each observed directory entry through a locale-stable normalized key. **Prevention:** Build one `Locale.ROOT` lowercase set from the canonical sensitive names, compare every observed name against it, and add the original spelling to the exclusion set so downstream exact membership remains correct. **Evidence:** `testProcessIgnoreFileTreatsSensitiveNamesCaseInsensitively` failed on test-only commit `472b916cd40f70693c4e1eb48956042a25353feb` (CI run `31469596932`) and passed with the source fix at `bb113d858ccfc42ddaecf6729749b238e5ade2d0` (CI run `31469921661`). + +## 2026-09-01 - [MEDIUM] TOCTOU (Time-of-Check to Time-of-Use) DoS in File.useLines +**Vulnerability:** `canRead()`로 파일 읽기 권한을 확인한 직후라도, `useLines()`로 파일을 열 때 권한이 변경되거나 파일이 삭제되면 처리되지 않은 예외가 발생하여 전체 크롤링 프로세스가 중단(DoS)될 수 있습니다. +**Learning:** 파일 상태를 검증(`canRead()`)하는 시점과 실제로 I/O 작업을 수행(`useLines()`)하는 시점 사이에는 간격이 존재하므로, 검증에만 의존하면 TOCTOU 취약점에 노출됩니다. +**Prevention:** 파일 I/O 작업 시 단일 시점의 상태 검증에만 의존하지 말고, 실제 I/O 호출부를 `try-catch (Exception)`으로 감싸서 예외 발생 시 애플리케이션 크래시 대신 우아하게 실패(Fail Securely)하도록 구현해야 합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 0972fa2c..94cbd3af 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -309,18 +309,22 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S if(ignore_file.isFile && !Files.isSymbolicLink(ignore_file.toPath()) && ignore_file.canRead() && ignore_file.length() <= 1048576){ val ignored_matchers = mutableListOf() - ignore_file.useLines { lines -> - for ((lineIndex, it) in lines.withIndex()) { - // 줄 수 제한이 패턴 수도 함께 상한(줄당 최대 1개 패턴)하므로 별도 패턴 카운터는 불필요 - if (lineIndex >= 1000) break - val pattern = it.trim() - if (pattern.isNotEmpty() && pattern.length <= 100) { - try { - ignored_matchers.add(java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$pattern")) - } catch (_: IllegalArgumentException) { + try { + ignore_file.useLines { lines -> + for ((lineIndex, it) in lines.withIndex()) { + // 줄 수 제한이 패턴 수도 함께 상한(줄당 최대 1개 패턴)하므로 별도 패턴 카운터는 불필요 + if (lineIndex >= 1000) break + val pattern = it.trim() + if (pattern.isNotEmpty() && pattern.length <= 100) { + try { + ignored_matchers.add(java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$pattern")) + } catch (_: IllegalArgumentException) { + } } } } + } catch (_: Exception) { + // 보안: TOCTOU 파일 접근 거부/삭제 예외 시 크래시(DoS) 방지 및 안전하게 무시 (Fail Securely) } // ⚡ Bolt Performance Optimization: 디렉토리 목록을 Set에 추가하기 위해 필터링만 할 때는 정렬이 불필요하므로 .sorted()를 제거하여 O(N log N) 오버헤드를 방지합니다. diff --git a/src/test/kotlin/html4tree/ToctouTest.kt b/src/test/kotlin/html4tree/ToctouTest.kt new file mode 100644 index 00000000..2bee4e24 --- /dev/null +++ b/src/test/kotlin/html4tree/ToctouTest.kt @@ -0,0 +1,38 @@ +package html4tree + +import org.junit.Test +import java.io.File +import kotlin.test.assertTrue +import kotlin.concurrent.thread +import java.nio.file.Files + +class ToctouTest { + @Test + fun testProcessIgnoreFileToctouExceptionRace() { + val tempDir = Files.createTempDirectory("toctoutest").toFile() + val ignoreFile = File(tempDir, ".html4ignore") + + var excluded: Set? = null + for (i in 0..2000) { + ignoreFile.writeText("test.txt") + val t = thread { ignoreFile.delete() } + excluded = process_ignore_file(tempDir, null) + t.join() + } + assertTrue(excluded?.contains("index.html") ?: false) + } + + @Test + fun testProcessIgnoreFileToctouException() { + val tempDir = Files.createTempDirectory("toctoutest2").toFile() + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText("test.txt") + + // Force an IOException during useLines + val method = java.io.File::class.java.getDeclaredMethod("setReadable", Boolean::class.java) + method.isAccessible = true + method.invoke(ignoreFile, false) + val excluded = process_ignore_file(tempDir, null) + assertTrue(excluded.contains("index.html")) + } +}