diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a885865d..fe90fc49 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`). + +## 2024-07-25 - [html4tree] TOCTOU (Time-of-Check to Time-of-Use) 방어로 인해 발생한 IOException 방지 +**Vulnerability:** 구성 파일(`.html4ignore`) 처리 중 canRead() 검사와 useLines() 호출 사이에 파일이 변경되거나 삭제될 경우 `java.io.FileNotFoundException` 등 `IOException`이 발생하여 애플리케이션 충돌(DoS)이 발생할 수 있는 TOCTOU 취약점이 발견되었습니다. +**Learning:** `isFile`, `isSymbolicLink`, `canRead` 등을 사용하여 파일 상태를 확인한 후(Time-of-Check), 실제로 파일을 읽거나 사용할 때(Time-of-Use)에는 해당 시점에 파일 시스템 상태가 변경될 수 있다는 점을 항상 고려해야 합니다. 특히 멀티 스레드나 병렬 처리 환경, 외부 사용자(또는 프로세스)가 개입할 수 있는 환경에서는 `try-catch`로 예외를 감싸야 합니다. +**Prevention:** `useLines` 또는 파일 I/O 작업을 수행할 때 `try-catch` 블록으로 `IOException`을 포착(catch)하여 우아하게 실패(Fail Securely)하도록 처리하고, 전체 애플리케이션 프로세스가 중단되지 않게 방어해야 합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 0972fa2c..c0f6cc72 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -309,18 +309,24 @@ 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 (_: java.io.IOException) { + // 보안 향상: TOCTOU(Time-of-Check to Time-of-Use) 방어. + // canRead() 확인 이후 useLines() 실행 직전에 파일 권한이 변경되거나 삭제될 경우 + // 발생하는 IOException을 무시하여 전체 크롤러 충돌(DoS)을 방지합니다. } // ⚡ 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..7fa5eb58 --- /dev/null +++ b/src/test/kotlin/html4tree/ToctouTest.kt @@ -0,0 +1,33 @@ +package html4tree + +import org.junit.Test +import org.junit.Assert.* +import java.io.File +import java.nio.file.Files + +class ToctouTest { + @Test + fun testProcessIgnoreFileToctouIoException() { + val tempDir = Files.createTempDirectory("test-toctou").toFile() + try { + val ignoreFile = File(tempDir, ".html4ignore") + val done = java.util.concurrent.atomic.AtomicBoolean(false) + val t = kotlin.concurrent.thread { + while (!done.get()) { + ignoreFile.writeText("*.txt") + ignoreFile.setReadable(true) + ignoreFile.setReadable(false) + ignoreFile.delete() + } + } + + for (i in 1..5000) { + process_ignore_file(tempDir, null) + } + done.set(true) + t.join() + } finally { + tempDir.deleteRecursively() + } + } +}