Skip to content
Open
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 @@ -88,3 +88,8 @@
**Vulnerability:** CSP 해시 불일치로 인한 인라인 스타일 차단
**Learning:** 브라우저는 인라인 스크립트와 스타일의 내부 텍스트(공백과 줄바꿈 포함)를 정확하게 해싱하여 Content-Security-Policy(CSP) 해시와 비교합니다. Kotlin의 멀티라인 문자열(`"""`)을 사용하여 템플릿에 콘텐츠를 주입할 때 암묵적인 여백이나 줄바꿈이 추가되면 최종 HTML 문자열이 변경되어 CSP 해시가 무효화됩니다.
**Prevention:** 콘텐츠를 해싱하기 전에 `.trimIndent()`를 적용하여 원본 문자열을 정규화하고, HTML 템플릿에 주입할 때 `<style>${exactContent}</style>`와 같이 공백 없이 주입하여 해시가 완벽하게 일치하도록 해야 합니다.

## 2024-08-08 - [html4tree] 원자적 파일 교체 (Atomic File Swap)
**Vulnerability:** 파일 시스템 TOCTOU(Time-of-Check to Time-of-Use) 및 심볼릭 링크 공격. `REPLACE_EXISTING`으로 파일을 직접 덮어쓰면 기존 파일이 심볼릭 링크일 경우 대상이 덮어쓰여 임의의 파일이 손상될 수 있습니다.
**Learning:** 안전하게 파일을 교체하려면 `ATOMIC_MOVE`를 시도하여 교체 중간에 끼어들 틈이나 링크 추적을 허용하지 않도록 하고 실패 시 `REPLACE_EXISTING`으로 우회하는 방법이 가장 효과적입니다.
**Prevention:** `Files.move()` 시 항상 `StandardCopyOption.ATOMIC_MOVE`를 사용하고 `AtomicMoveNotSupportedException`을 안전하게 처리하도록 구성하십시오.
14 changes: 12 additions & 2 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,22 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
return files_to_exclude
}

fun write_index_file(curr_dir: File, content: String) {
fun write_index_file(
curr_dir: File,
content: String,
moveOp: (java.nio.file.Path, java.nio.file.Path) -> Unit = { src, dest ->
Files.move(src, dest, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
}
) {
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 {
moveOp(tempPath, indexPath)
} catch (e: java.nio.file.AtomicMoveNotSupportedException) {
Files.move(tempPath, indexPath, StandardCopyOption.REPLACE_EXISTING)
}
} finally {
Files.deleteIfExists(tempPath)
}
Expand Down
14 changes: 14 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,20 @@ class MainTest {
assertTrue(leftoverTemp.isEmpty(), "temporary index file should be cleaned up on failure")
}

@Test
fun testWriteIndexFileAtomicException() {
val localTempDir = Files.createTempDirectory("test_atomic").toFile()
try {
write_index_file(localTempDir, "content") { _, _ ->
throw java.nio.file.AtomicMoveNotSupportedException("src", "dest", "mock exception")
}
val indexPath = localTempDir.toPath().resolve("index.html")
assertTrue(Files.exists(indexPath))
} finally {
localTempDir.deleteRecursively()
}
}

@Test
fun testProcessDirReplacesIndexSymlinkWithoutTouchingTarget() {
val targetFile = File(tempDir, "target.txt")
Expand Down
Loading