From b628b09d02e6d39de5f921f46031ae2fe073791b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:02:32 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. --- .jules/sentinel.md | 4 +++ parse_jacoco.py | 11 ++++++ src/main/kotlin/html4tree/main.kt | 49 +++++++++++++++++++-------- src/test/kotlin/html4tree/MainTest.kt | 38 +++++++++++++++++---- 4 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 parse_jacoco.py diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a885865d..c72bd42f 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -99,3 +99,7 @@ **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`). +## 2025-05-24 - [CRITICAL] Fail-closed Security Policy Files (.html4ignore) +**Vulnerability:** TOCTOU and Policy bypass via broken/invalid .html4ignore symlinks or directories. +**Learning:** Security policy files (like .html4ignore) were previously ignoring unreadable or invalid variants (e.g. symlinks, directories, unreadable files). This meant that if a policy file couldn't be read (due to TOCTOU manipulation or broken symlinks), the application failed open and generated the index anyway, potentially exposing sensitive files that should have been excluded. +**Prevention:** Implement `IgnoreFileReadException` and fail-closed architecture. When encountering a security policy file, check if it exists or is a symlink first, then explicitly verify it's a valid readable file. If invalid/unreadable, throw the exception to abruptly stop traversal (suppress publication) for that specific directory subtree. diff --git a/parse_jacoco.py b/parse_jacoco.py new file mode 100644 index 00000000..f4545db8 --- /dev/null +++ b/parse_jacoco.py @@ -0,0 +1,11 @@ +import csv + +try: + with open("build/reports/jacoco/test/jacocoTestReport.csv", "r") as f: + reader = csv.DictReader(f) + for row in reader: + if int(row["INSTRUCTION_MISSED"]) > 0: + total = int(row["INSTRUCTION_COVERED"]) + int(row["INSTRUCTION_MISSED"]) + print(f"{row['PACKAGE']}.{row['CLASS']}.{row['METHOD']}: Missed {row['INSTRUCTION_MISSED']}/{total} instructions") +except Exception as e: + print(f"Error: {e}") diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 0972fa2c..fe96da6d 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -200,20 +200,27 @@ internal fun crawl_directories( val dirFilesNames = dirFiles?.let { files -> Array(files.size) { index -> files[index].name } } - val exclude = processIgnoreFile(lle.file, dirFilesNames) - - if(maxLevel == -1 || currentLevel <= maxLevel) - processDirectory(lle.file, exclude, dirFiles) - - if(maxLevel == -1 || currentLevel < maxLevel) { - dirFiles?.forEach { - // ⚑ Bolt Performance Optimization: Short-circuit OS stat calls - // by checking cheap in-memory string exclusion rules first - if(!it.name.isHiddenFile() && it.name !in exclude) { - val childAttrs = readAttributes(it) - if(childAttrs != null && childAttrs.isDirectory && !childAttrs.isSymbolicLink) { - val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key) - ll.push(childEntry) + + val exclude = try { + processIgnoreFile(lle.file, dirFilesNames) + } catch (e: IgnoreFileReadException) { + null + } + + if (exclude != null) { + if(maxLevel == -1 || currentLevel <= maxLevel) + processDirectory(lle.file, exclude, dirFiles) + + if(maxLevel == -1 || currentLevel < maxLevel) { + dirFiles?.forEach { + // ⚑ Bolt Performance Optimization: Short-circuit OS stat calls + // by checking cheap in-memory string exclusion rules first + if(!it.name.isHiddenFile() && it.name !in exclude) { + val childAttrs = readAttributes(it) + if(childAttrs != null && childAttrs.isDirectory && !childAttrs.isSymbolicLink) { + val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key) + ll.push(childEntry) + } } } } @@ -293,6 +300,13 @@ fun String.urlEncodePath(): String { return encoded?.toString() ?: this } +/** + * Exception thrown when a security policy file (e.g., .html4ignore) is detected + * but cannot be read or disappears (TOCTOU race condition). + * Enforces fail-closed behavior to suppress directory publication and traversal. + */ +class IgnoreFileReadException(message: String) : Exception(message) + fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): Set { val ignore_filename = ".html4ignore" @@ -303,6 +317,13 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S val files_to_exclude = mutableSetOf() + val ignoreExists = ignore_file.exists() || Files.isSymbolicLink(ignore_file.toPath()) + if (ignoreExists) { + if (!ignore_file.isFile || Files.isSymbolicLink(ignore_file.toPath()) || !ignore_file.canRead()) { + throw IgnoreFileReadException("Policy file $ignore_filename is inaccessible or invalid. Failing closed to prevent TOCTOU bypass.") + } + } + // λ³΄μ•ˆ ν–₯상: .html4ignore 파일이 일반 νŒŒμΌμΈμ§€ ν™•μΈν•˜κ³ , 심볼릭 링크인 경우 λ¬΄μ‹œν•˜μ—¬ DoS 및 경둜 μ‘°μž‘μ„ λ°©μ§€ν•©λ‹ˆλ‹€. // λ³΄μ•ˆ ν–₯상: 파일 크기(1MB μ œν•œ) 및 쀄 수(1000쀄), μ •κ·œμ‹ 길이(100자)λ₯Ό μ œν•œν•˜μ—¬ ReDoS 및 λ©”λͺ¨λ¦¬ 고갈(OOM) λ°©μ§€ // λ³΄μ•ˆ ν–₯상: κΆŒν•œμ΄ μ—†λŠ” 파일 μ ‘κ·Ό μ‹œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έ(DoS)λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄ canRead() μΆ”κ°€ 확인 diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 5b76cc5d..6025b30d 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -718,9 +718,29 @@ class MainTest { val ignoreDir = File(tempDir, ".html4ignore") ignoreDir.mkdir() - // This should not crash or parse the directory - val excluded = process_ignore_file(tempDir, null) - assertTrue(excluded.contains("index.html")) + // This should fail-closed and throw IgnoreFileReadException + var thrown = false + try { + process_ignore_file(tempDir, null) + } catch (e: IgnoreFileReadException) { + thrown = true + } + assertTrue(thrown, "Expected IgnoreFileReadException to be thrown") + } + + @Test + fun testCrawlDirectoriesIgnoreFileReadException() { + val ignoreDir = File(tempDir, ".html4ignore") + ignoreDir.mkdir() + val ll = LinkedList() + val topEntry = LinkedListEntry(tempDir, 0, read_file_identity(tempDir).key) + ll.push(topEntry) + + crawl_directories( + ll = ll, + maxLevel = -1, + processIgnoreFile = { _, _ -> throw IgnoreFileReadException("mock") } + ) } @Test @@ -762,10 +782,14 @@ class MainTest { File(tempDir, "test.txt").createNewFile() - // Should ignore the symlink and NOT parse it - val excluded = process_ignore_file(tempDir, null) - assertFalse(excluded.contains("test.txt")) - assertTrue(excluded.contains("index.html")) + // Should fail-closed and throw IgnoreFileReadException + var thrown = false + try { + process_ignore_file(tempDir, null) + } catch (e: IgnoreFileReadException) { + thrown = true + } + assertTrue(thrown, "Expected IgnoreFileReadException to be thrown") } @Test From 74f289ee1a405dd20d16ec8db23af758a7d31936 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:17:29 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. From 2383eb6a9cdd571bd4d3dbb4a09450f76aac3d30 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:15:26 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. From 7c020d1ff2b79336c2c653f6150f7849ee206639 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 23 Sep 2026 20:04:51 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. From 35bc8ec90c0c9654fd13b20026e4b6a62ed64032 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 24 Sep 2026 05:21:42 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. From b467d826f0c21c2271dc8c4c694566f3a474f40c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:34:02 +0000 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. From 7a4d186aa94f5acaf305e863664020a45ae1a46c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:34:30 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. From a98f33eac4d6814be710ba410793052be7841c2a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:34:07 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인. --- parse_jacoco.py | 11 ---------- src/main/kotlin/html4tree/main.kt | 2 +- src/test/kotlin/html4tree/MainTest.kt | 31 +++++++++++++++------------ 3 files changed, 18 insertions(+), 26 deletions(-) delete mode 100644 parse_jacoco.py diff --git a/parse_jacoco.py b/parse_jacoco.py deleted file mode 100644 index f4545db8..00000000 --- a/parse_jacoco.py +++ /dev/null @@ -1,11 +0,0 @@ -import csv - -try: - with open("build/reports/jacoco/test/jacocoTestReport.csv", "r") as f: - reader = csv.DictReader(f) - for row in reader: - if int(row["INSTRUCTION_MISSED"]) > 0: - total = int(row["INSTRUCTION_COVERED"]) + int(row["INSTRUCTION_MISSED"]) - print(f"{row['PACKAGE']}.{row['CLASS']}.{row['METHOD']}: Missed {row['INSTRUCTION_MISSED']}/{total} instructions") -except Exception as e: - print(f"Error: {e}") diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index fe96da6d..4473e97a 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -319,7 +319,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S val ignoreExists = ignore_file.exists() || Files.isSymbolicLink(ignore_file.toPath()) if (ignoreExists) { - if (!ignore_file.isFile || Files.isSymbolicLink(ignore_file.toPath()) || !ignore_file.canRead()) { + if (!ignore_file.isFile || Files.isSymbolicLink(ignore_file.toPath()) || !ignore_file.canRead() || ignore_file.length() > 1048576) { throw IgnoreFileReadException("Policy file $ignore_filename is inaccessible or invalid. Failing closed to prevent TOCTOU bypass.") } } diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 6025b30d..e744cce0 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -719,13 +719,9 @@ class MainTest { ignoreDir.mkdir() // This should fail-closed and throw IgnoreFileReadException - var thrown = false - try { + assertFailsWith("Expected IgnoreFileReadException to be thrown") { process_ignore_file(tempDir, null) - } catch (e: IgnoreFileReadException) { - thrown = true } - assertTrue(thrown, "Expected IgnoreFileReadException to be thrown") } @Test @@ -743,6 +739,17 @@ class MainTest { ) } + @Test + fun testIgnoreFileTooLarge() { + val ignoreFile = File(tempDir, ".html4ignore") + val largeBytes = ByteArray(1048576 + 1) + ignoreFile.writeBytes(largeBytes) + + assertFailsWith("Expected IgnoreFileReadException to be thrown") { + process_ignore_file(tempDir, null) + } + } + @Test fun testProcessIgnoreFileDosProtection() { val ignoreFile = File(tempDir, ".html4ignore") @@ -783,13 +790,9 @@ class MainTest { File(tempDir, "test.txt").createNewFile() // Should fail-closed and throw IgnoreFileReadException - var thrown = false - try { + assertFailsWith("Expected IgnoreFileReadException to be thrown") { process_ignore_file(tempDir, null) - } catch (e: IgnoreFileReadException) { - thrown = true } - assertTrue(thrown, "Expected IgnoreFileReadException to be thrown") } @Test @@ -801,10 +804,10 @@ class MainTest { File(tempDir, "test.txt").createNewFile() - // Should ignore the file because it's too large - val excluded = process_ignore_file(tempDir, null) - assertFalse(excluded.contains("test.txt")) - assertTrue(excluded.contains("index.html")) + // Should fail-closed and throw IgnoreFileReadException + assertFailsWith("Expected IgnoreFileReadException to be thrown") { + process_ignore_file(tempDir, null) + } } @Test From 2599fe4bcdd71b30669723134348704b76d1bcd2 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:15:12 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fail-closed=20Security=20Policy=20Files=20(.html4ignore)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 심각도: CRITICAL πŸ’‘ 취약점: .html4ignore 파일이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ κΉ¨μ§„ 심볼릭 링크일 λ•Œ, κΆŒν•œμ΄ μ—†μ–΄ 읽을 수 없을 λ•Œ ν•΄λ‹Ή 디렉토리λ₯Ό μ—΄μ–΄ λͺ¨λ“  νŒŒμΌμ„ νΌλΈ”λ¦¬μ‹±ν•˜λŠ” Fail-Open 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이둜 인해 TOCTOU(Time-of-check to time-of-use) κ³΅κ²©μ΄λ‚˜ 심볼릭 링크 μ‘°μž‘μœΌλ‘œ λ―Όκ°ν•œ 파일이 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€. 🎯 영ν–₯: λ””λ ‰ν† λ¦¬μ˜ λ―Όκ°ν•œ νŒŒμΌλ“€μ΄ μ˜λ„μΉ˜ μ•Šκ²Œ 외뢀에 λ…ΈμΆœλ  수 μžˆμŠ΅λ‹ˆλ‹€ (정보 λ…ΈμΆœ, 디렉토리 탐색 우회). πŸ”§ μˆ˜μ •μ‚¬ν•­: `IgnoreFileReadException`을 λ„μž…ν•˜κ³ , `process_ignore_file`μ—μ„œ 파일 μ ‘κ·Ό μ‹€νŒ¨λ‚˜ κΉ¨μ§„ 심볼릭 링크 λ“±μ˜ 상황이 λ°œμƒν•˜λ©΄ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. `crawl_directories`μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό 작으면 ν•΄λ‹Ή λ””λ ‰ν† λ¦¬μ˜ νŒŒμ‹±(디렉토리 λ‚΄λΆ€ 퍼블리싱)을 μ€‘μ§€ν•˜μ—¬ Fail-Closed λ˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. βœ… 검증: 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό 보μž₯ν•˜λŠ” κ΄€λ ¨ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `jacocoTestCoverageVerification` 톡과 확인.