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 @@ -83,3 +83,8 @@
**Vulnerability:** 정적 HTML 생성 도구에서 매번 다른 Nonce를 동적으로 생성하여 CSP에 적용하는 것은, 캐싱 효율을 저하시킬 뿐만 아니라 정적 배포 환경(예: GitHub Pages 등)에서 올바른 보안 정책 수립을 방해할 수 있는 안티 패턴입니다.
**Learning:** 정적으로 고정된 인라인 스타일이나 스크립트에는 난수화된 Nonce보다 콘텐츠 자체의 해시(SHA-256 등)를 사용하는 것이 안전하고 일관된 방식임을 배웠습니다.
**Prevention:** 자동 생성되는 정적 HTML의 콘텐츠 보안 정책(CSP)에는 `style-src 'sha256-<HASH>'` 방식을 적용하고, `<style>` 태그에서 불필요한 `nonce` 속성을 제거하여 브라우저의 무결성 검증 기능을 적극 활용하십시오.

## 2024-07-25 - [html4tree] 원자적 파일 교체를 통한 읽기 일관성 보장 (ATOMIC_MOVE)
**Vulnerability:** 파일을 직접 교체할 때 원자적 복사 옵션을 사용하지 않으면, 파일이 덮어쓰여지는 도중(즉, 파일의 일부만 쓰여진 상태)에 다른 프로세스나 클라이언트가 해당 파일을 읽게 되어 불완전한 데이터를 처리하게 되는 레이스 컨디션(TOCTOU) 및 부분 읽기 취약점이 발생할 수 있습니다.
**Learning:** `java.nio.file.Files.move()`를 사용할 때 `StandardCopyOption.REPLACE_EXISTING`만 지정하면 운영체제와 파일 시스템에 따라 파일 덮어쓰기가 원자적(atomic)으로 이루어지지 않을 수 있습니다.
**Prevention:** 파일 쓰기 작업 후 안전하게 교체하기 위해 항상 `StandardCopyOption.ATOMIC_MOVE`를 시도하고, 파일 시스템 제약(예: 다른 디스크 파티션 간 이동)으로 인해 지원되지 않는 경우에만 예외(`AtomicMoveNotSupportedException`)를 잡아 `REPLACE_EXISTING`으로 폴백(fallback)하도록 구현해야 합니다.
Comment on lines +88 to +90
12 changes: 10 additions & 2 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,20 @@ 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,
moveFile: (java.nio.file.Path, java.nio.file.Path, Array<StandardCopyOption>) -> java.nio.file.Path = { source, target, options -> Files.move(source, target, *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.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE))
} catch (e: java.nio.file.AtomicMoveNotSupportedException) {
moveFile(tempPath, indexPath, arrayOf(StandardCopyOption.REPLACE_EXISTING))
}
} finally {
Files.deleteIfExists(tempPath)
}
Expand Down
15 changes: 15 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ class MainTest {
assertTrue(htmlContent.contains("margin: 0 auto;"))
}

@Test
fun testWriteIndexFileAtomicMoveFallback() {
var fallbackCalled = false
val moveFileMock: (java.nio.file.Path, java.nio.file.Path, Array<java.nio.file.StandardCopyOption>) -> java.nio.file.Path = { source, target, options ->
if (options.contains(java.nio.file.StandardCopyOption.ATOMIC_MOVE)) {
throw java.nio.file.AtomicMoveNotSupportedException(source.toString(), target.toString(), "Mock unsupported")
}
fallbackCalled = true
java.nio.file.Files.move(source, target, *options)
}
write_index_file(tempDir, "fallback content", moveFileMock)
assertTrue(fallbackCalled, "Fallback to REPLACE_EXISTING should be called")
assertTrue(File(tempDir, "index.html").readText().contains("fallback content"))
Comment on lines +350 to +360
}

@Test
fun testWriteIndexFileCleansUpTempFileOnFailure() {
// Files.move cannot replace a non-empty directory, so this drives the
Expand Down
Loading