diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 6ecf72f..a331902 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -83,3 +83,15 @@ **Vulnerability:** 정적 HTML 생성 도구에서 매번 다른 Nonce를 동적으로 생성하여 CSP에 적용하는 것은, 캐싱 효율을 저하시킬 뿐만 아니라 정적 배포 환경(예: GitHub Pages 등)에서 올바른 보안 정책 수립을 방해할 수 있는 안티 패턴입니다. **Learning:** 정적으로 고정된 인라인 스타일이나 스크립트에는 난수화된 Nonce보다 콘텐츠 자체의 해시(SHA-256 등)를 사용하는 것이 안전하고 일관된 방식임을 배웠습니다. **Prevention:** 자동 생성되는 정적 HTML의 콘텐츠 보안 정책(CSP)에는 `style-src 'sha256-'` 방식을 적용하고, ` tags. When using string interpolation in Kotlin multiline strings, any surrounding whitespace inside the tags alters the final output, rendering the CSP hash invalid and blocking the style. +**Prevention:** When injecting content into a + """ val index_top = """ diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 1349471..601aeeb 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -531,6 +531,41 @@ class MainTest { assertEquals("A1z", "A1z".urlEncodePath()) } + @Test + fun testSymlinkedAncestorRejection() { + val subdir = File(tempDir, "real_dir") + subdir.mkdir() + val symlinkDir = File(tempDir, "symlink_dir") + try { + Files.createSymbolicLink(symlinkDir.toPath(), subdir.toPath()) + } catch (e: Exception) { + Assume.assumeTrue("Symlink creation not supported in this environment", false) + } + val targetInSymlink = File(symlinkDir, "target_dir") + targetInSymlink.mkdir() + + process_dir(targetInSymlink) + val indexFile = File(targetInSymlink, "index.html") + assertFalse(indexFile.exists(), "Should not write index file when ancestor is a symlink") + } + + @Test + fun testCspHashMatch() { + val tempDir2 = File(tempDir, "cspDir") + tempDir2.mkdir() + process_dir(tempDir2) + val indexFile = File(tempDir2, "index.html") + val content = indexFile.readText() + val hashRegex = Regex("style-src '([^']+)'") + val match = hashRegex.find(content) + val actualHash = match?.groupValues?.get(1) + val styleContentRegex = Regex("") + val styleMatch = styleContentRegex.find(content) + val styleContent = styleMatch?.groupValues?.get(1) + val expectedHash = "sha256-" + java.util.Base64.getEncoder().encodeToString(java.security.MessageDigest.getInstance("SHA-256").digest(styleContent!!.toByteArray(Charsets.UTF_8))) + assertEquals(expectedHash, actualHash) + } + @Test fun testUrlEncodePathReservedHexCoverage() { // Need characters that produce hex digit > 9 to hit the `else` branch of `if (hex1 < 10)` and `if (hex2 < 10)`.