diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 6ecf72f1..a82186ca 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -83,3 +83,8 @@ **Vulnerability:** 정적 HTML 생성 도구에서 매번 다른 Nonce를 동적으로 생성하여 CSP에 적용하는 것은, 캐싱 효율을 저하시킬 뿐만 아니라 정적 배포 환경(예: GitHub Pages 등)에서 올바른 보안 정책 수립을 방해할 수 있는 안티 패턴입니다. **Learning:** 정적으로 고정된 인라인 스타일이나 스크립트에는 난수화된 Nonce보다 콘텐츠 자체의 해시(SHA-256 등)를 사용하는 것이 안전하고 일관된 방식임을 배웠습니다. **Prevention:** 자동 생성되는 정적 HTML의 콘텐츠 보안 정책(CSP)에는 `style-src 'sha256-'` 방식을 적용하고, ``) HTML에 인젝트하여 완전한 해시 일치를 보장하십시오. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index b4558624..08c6a8da 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -28,6 +28,78 @@ fun main(args: Array) = Html4tree().main(args) internal data class FileIdentity(val key: Any?, val readable: Boolean) +private val cssContent = """ + body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.5; + padding: 1rem; + color: #1f2328; + } + main { + max-width: 800px; + margin: 0 auto; + } + ul { + list-style-type: none; + padding-left: 0; + } + a.dir-link { + display: flex; + align-items: flex-start; + gap: 0.5rem; + width: 100%; + overflow-wrap: anywhere; + box-sizing: border-box; + } + .icon { + flex-shrink: 0; + width: 1.25rem; + text-align: center; + } + a { + padding: 0.5rem; + text-decoration: none; + color: #0969da; + border-radius: 4px; + transition: background-color 0.2s ease, outline-color 0.2s ease; + } + a:hover, a:focus-visible { + background-color: #f6f8fa; + text-decoration: underline; + outline: 2px solid #0969da; + outline-offset: -2px; + } + @media (prefers-reduced-motion: reduce) { + a { + transition: none; + } + } + @media (prefers-color-scheme: dark) { + body { + background-color: #0d1117; + color: #c9d1d9; + } + a { + color: #58a6ff; + } + a:hover, a:focus-visible { + background-color: #161b22; + outline-color: #58a6ff; + } + } + .empty-dir { + padding: 0.5rem; + opacity: 0.7; + font-style: italic; + } +""".trimIndent() + +private val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8))) + +private val css = """ + + """.trimIndent() + internal fun read_file_identity(file: File): FileIdentity { return try { val attrs = Files.readAttributes(file.toPath(), BasicFileAttributes::class.java, LinkOption.NOFOLLOW_LINKS) @@ -244,79 +316,6 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array val exclude: Set = excludeSet ?: process_ignore_file(curr_dir) - val cssContent = """ - body { - font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - line-height: 1.5; - padding: 1rem; - color: #1f2328; - } - main { - max-width: 800px; - margin: 0 auto; - } - ul { - list-style-type: none; - padding-left: 0; - } - a.dir-link { - display: flex; - align-items: flex-start; - gap: 0.5rem; - width: 100%; - overflow-wrap: anywhere; - box-sizing: border-box; - } - .icon { - flex-shrink: 0; - width: 1.25rem; - text-align: center; - } - a { - padding: 0.5rem; - text-decoration: none; - color: #0969da; - border-radius: 4px; - transition: background-color 0.2s ease, outline-color 0.2s ease; - } - a:hover, a:focus-visible { - background-color: #f6f8fa; - text-decoration: underline; - outline: 2px solid #0969da; - outline-offset: -2px; - } - @media (prefers-reduced-motion: reduce) { - a { - transition: none; - } - } - @media (prefers-color-scheme: dark) { - body { - background-color: #0d1117; - color: #c9d1d9; - } - a { - color: #58a6ff; - } - a:hover, a:focus-visible { - background-color: #161b22; - outline-color: #58a6ff; - } - } - .empty-dir { - padding: 0.5rem; - opacity: 0.7; - font-style: italic; - } - """ - - val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8))) - - val css = """ - - """ - val index_top = """