Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)하도록 처리하고, 전체 애플리케이션 프로세스가 중단되지 않게 방어해야 합니다.
24 changes: 15 additions & 9 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -309,18 +309,24 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
if(ignore_file.isFile && !Files.isSymbolicLink(ignore_file.toPath()) && ignore_file.canRead() && ignore_file.length() <= 1048576){
val ignored_matchers = mutableListOf<java.nio.file.PathMatcher>()

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) 오버헤드를 방지합니다.
Expand Down
33 changes: 33 additions & 0 deletions src/test/kotlin/html4tree/ToctouTest.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Loading