diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 6ecf72f..5db89ac 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -83,3 +83,7 @@ **Vulnerability:** 정적 HTML 생성 도구에서 매번 다른 Nonce를 동적으로 생성하여 CSP에 적용하는 것은, 캐싱 효율을 저하시킬 뿐만 아니라 정적 배포 환경(예: GitHub Pages 등)에서 올바른 보안 정책 수립을 방해할 수 있는 안티 패턴입니다. **Learning:** 정적으로 고정된 인라인 스타일이나 스크립트에는 난수화된 Nonce보다 콘텐츠 자체의 해시(SHA-256 등)를 사용하는 것이 안전하고 일관된 방식임을 배웠습니다. **Prevention:** 자동 생성되는 정적 HTML의 콘텐츠 보안 정책(CSP)에는 `style-src 'sha256-'` 방식을 적용하고, ` - """ + val css = "" val index_top = """ diff --git a/src/test/kotlin/html4tree/CspTest.kt b/src/test/kotlin/html4tree/CspTest.kt new file mode 100644 index 0000000..c097671 --- /dev/null +++ b/src/test/kotlin/html4tree/CspTest.kt @@ -0,0 +1,85 @@ +package html4tree + +import org.junit.Test +import java.security.MessageDigest +import java.util.Base64 +import kotlin.test.assertTrue + +class CspTest { + @Test + fun testCspHashMatchesInjectedStyle() { + 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() + + val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8))) + + val css = "" + val insideStyle = css.substringAfter("") + val styleHash2 = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(insideStyle.toByteArray(Charsets.UTF_8))) + + assertTrue(styleHash == styleHash2, "The CSP hash must exactly match the injected inline style content.") + } +}