From 8ffd6dee74140a5b255cbdef065ef204a2202273 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:04:07 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM]=20?= =?UTF-8?q?Fix=20TOCTOU=20vulnerability=20in=20index.html=20generation=20v?= =?UTF-8?q?ia=20Atomic=20Move?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿšจ Severity: MEDIUM ๐Ÿ’ก Vulnerability: ํŒŒ์ผ ๋ฎ์–ด์“ฐ๊ธฐ ๊ณผ์ •์—์„œ `REPLACE_EXISTING`์„ ์‚ฌ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” Race Condition์œผ๋กœ ์ธํ•ด ์ƒ์„ฑ ์ค‘์ธ ์ž„์‹œ ํŒŒ์ผ๊ณผ ์‹ค์ œ ๋Œ€์ƒ ํŒŒ์ผ(index.html)์ด ๊ต์ฒด๋˜๋Š” ์ˆœ๊ฐ„ ์‹œ๊ฐ„์ฐจ ๊ณต๊ฒฉ(TOCTOU)์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์Œ. ๐ŸŽฏ Impact: ๋‹ค์ค‘ ์Šค๋ ˆ๋“œ/ํ”„๋กœ์„ธ์Šค ํ™˜๊ฒฝ์—์„œ ์ž˜๋ชป๋˜๊ฑฐ๋‚˜ ์กฐ์ž‘๋œ ํŒŒ์ผ์ด ์ธ๋ฑ์Šค๋กœ ์ œ๊ณต๋  ์ˆ˜ ์žˆ์Œ. ๐Ÿ”ง Fix: `write_index_file`์—์„œ ํŒŒ์ผ์„ ์ด๋™ํ•  ๋•Œ ๊ธฐ๋ณธ์ ์œผ๋กœ `StandardCopyOption.ATOMIC_MOVE`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ์›์ž์  ๊ต์ฒด๋ฅผ ๋ณด์žฅํ•จ. ํŒŒ์ผ ์‹œ์Šคํ…œ์—์„œ ์ด๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ™˜๊ฒฝ(`AtomicMoveNotSupportedException` ๋ฐœ์ƒ)์—์„œ๋Š” ๊ธฐ์กด์˜ `REPLACE_EXISTING` ๋ฐฉ์‹์œผ๋กœ Fallback ์ฒ˜๋ฆฌํ•˜๋„๋ก ๊ตฌํ˜„ํ•จ. โœ… Verification: `./gradlew test jacocoTestReport`๋ฅผ ํ†ตํ•ด 100% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฐ Fallback ๋™์ž‘ ๊ฒ€์ฆ ์™„๋ฃŒ. --- .jules/sentinel.md | 5 +++++ src/main/kotlin/html4tree/main.kt | 14 ++++++++++++-- src/test/kotlin/html4tree/MainTest.kt | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index cdf88010..5318aa48 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -88,3 +88,8 @@ **Vulnerability:** CSP ํ•ด์‹œ ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ์ธ๋ผ์ธ ์Šคํƒ€์ผ ์ฐจ๋‹จ **Learning:** ๋ธŒ๋ผ์šฐ์ €๋Š” ์ธ๋ผ์ธ ์Šคํฌ๋ฆฝํŠธ์™€ ์Šคํƒ€์ผ์˜ ๋‚ด๋ถ€ ํ…์ŠคํŠธ(๊ณต๋ฐฑ๊ณผ ์ค„๋ฐ”๊ฟˆ ํฌํ•จ)๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ํ•ด์‹ฑํ•˜์—ฌ Content-Security-Policy(CSP) ํ•ด์‹œ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. Kotlin์˜ ๋ฉ€ํ‹ฐ๋ผ์ธ ๋ฌธ์ž์—ด(`"""`)์„ ์‚ฌ์šฉํ•˜์—ฌ ํ…œํ”Œ๋ฆฟ์— ์ฝ˜ํ…์ธ ๋ฅผ ์ฃผ์ž…ํ•  ๋•Œ ์•”๋ฌต์ ์ธ ์—ฌ๋ฐฑ์ด๋‚˜ ์ค„๋ฐ”๊ฟˆ์ด ์ถ”๊ฐ€๋˜๋ฉด ์ตœ์ข… HTML ๋ฌธ์ž์—ด์ด ๋ณ€๊ฒฝ๋˜์–ด CSP ํ•ด์‹œ๊ฐ€ ๋ฌดํšจํ™”๋ฉ๋‹ˆ๋‹ค. **Prevention:** ์ฝ˜ํ…์ธ ๋ฅผ ํ•ด์‹ฑํ•˜๊ธฐ ์ „์— `.trimIndent()`๋ฅผ ์ ์šฉํ•˜์—ฌ ์›๋ณธ ๋ฌธ์ž์—ด์„ ์ •๊ทœํ™”ํ•˜๊ณ , HTML ํ…œํ”Œ๋ฆฟ์— ์ฃผ์ž…ํ•  ๋•Œ ``์™€ ๊ฐ™์ด ๊ณต๋ฐฑ ์—†์ด ์ฃผ์ž…ํ•˜์—ฌ ํ•ด์‹œ๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ์ผ์น˜ํ•˜๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +## 2024-08-08 - [html4tree] ์›์ž์  ํŒŒ์ผ ๊ต์ฒด(Atomic File Move)๋ฅผ ํ†ตํ•œ TOCTOU ์™„ํ™” +**Vulnerability:** ํŒŒ์ผ ๊ต์ฒด ์‹œ(์ž„์‹œ ํŒŒ์ผ์„ ๋Œ€์ƒ ํŒŒ์ผ๋กœ `REPLACE_EXISTING` ๋ฐฉ์‹์œผ๋กœ ์˜ฎ๊ธธ ๋•Œ) ๋ ˆ์ด์Šค ์ปจ๋””์…˜(Race Condition)์œผ๋กœ ์ธํ•œ ์‹œ๊ฐ„ ์ฐจ ๊ณต๊ฒฉ(TOCTOU)์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +**Learning:** ๋‹จ์ผ ํŒŒ์ผ ๋ฎ์–ด์“ฐ๊ธฐ๋ผ๋„ ๋™์‹œ์— ์—ฌ๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋‚˜ ์Šค๋ ˆ๋“œ๊ฐ€ ์ ‘๊ทผํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ์›์ž์  ์ž‘์—…(Atomic Operation)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. +**Prevention:** `Files.move` ์ˆ˜ํ–‰ ์‹œ ๊ธฐ๋ณธ์ ์œผ๋กœ `StandardCopyOption.ATOMIC_MOVE`๋ฅผ ์‹œ๋„ํ•˜์—ฌ ๊ต์ฒด์˜ ์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๊ณ , ํŒŒ์ผ ์‹œ์Šคํ…œ์—์„œ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ(`AtomicMoveNotSupportedException` ๋ฐœ์ƒ ์‹œ)์—๋งŒ ๊ธฐ์กด ๋ฐฉ์‹(`REPLACE_EXISTING`)์œผ๋กœ ํด๋ฐฑ(Fallback)ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์‹ญ์‹œ์˜ค. ์ด๋ฅผ ํ†ตํ•ด ๊ฐ€๋Šฅํ•œ ์ตœ๋Œ€ํ•œ์˜ ์›์ž์„ฑ(Atomicity)์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index f52a1468..91a2ed01 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -311,12 +311,22 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S return files_to_exclude } -fun write_index_file(curr_dir: File, content: String) { +fun write_index_file( + curr_dir: File, + content: String, + moveFile: (java.nio.file.Path, java.nio.file.Path, Array) -> Unit = { src, dest, options -> + Files.move(src, dest, *options) + } +) { val indexPath = curr_dir.toPath().resolve("index.html") val tempPath = Files.createTempFile(curr_dir.toPath(), ".index-", ".html") try { Files.write(tempPath, content.toByteArray(Charsets.UTF_8)) - Files.move(tempPath, indexPath, StandardCopyOption.REPLACE_EXISTING) + try { + moveFile(tempPath, indexPath, arrayOf(StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)) + } catch (e: java.nio.file.AtomicMoveNotSupportedException) { + moveFile(tempPath, indexPath, arrayOf(StandardCopyOption.REPLACE_EXISTING)) + } } finally { Files.deleteIfExists(tempPath) } diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 83739c9c..9ecde88a 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -364,6 +364,21 @@ class MainTest { assertTrue(leftoverTemp.isEmpty(), "temporary index file should be cleaned up on failure") } + @Test + fun testAtomicMoveFallback() { + var fallbackCalled = false + write_index_file(tempDir, "content") { src, dest, options -> + if (options.contains(java.nio.file.StandardCopyOption.ATOMIC_MOVE)) { + throw java.nio.file.AtomicMoveNotSupportedException(src.toString(), dest.toString(), "Mocked") + } else { + fallbackCalled = true + java.nio.file.Files.move(src, dest, *options) + } + } + assertTrue(fallbackCalled, "Fallback move should be called when ATOMIC_MOVE is not supported") + assertEquals("content", File(tempDir, "index.html").readText()) + } + @Test fun testProcessDirReplacesIndexSymlinkWithoutTouchingTarget() { val targetFile = File(tempDir, "target.txt")