From d5f332311e199e3517272851e8cc3835d51a277b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:54:30 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20?= =?UTF-8?q?=EC=9D=B8=EB=9D=BC=EC=9D=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20CSP?= =?UTF-8?q?=20=ED=95=B4=EC=8B=9C=20=EA=B3=84=EC=82=B0=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `cssContent`에 `.trimIndent()`를 적용하여 선행/후행 공백을 제거함 - HTML 템플릿에 `cssContent`를 주입할 때 `` 형식으로 수정함 - CSP 해시 계산 시 `cssContent.toByteArray()`가 브라우저가 인식하는 실제 인라인 스타일 문자열과 정확히 일치하게 됨 - CSP 해시 계산 및 검증을 위한 단위 테스트(`CspTest.kt`) 추가 --- .jules/sentinel.md | 4 ++ src/main/kotlin/html4tree/main.kt | 7 +-- src/test/kotlin/html4tree/CspTest.kt | 85 ++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/test/kotlin/html4tree/CspTest.kt 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.") + } +}