๐ก๏ธ Sentinel: [MEDIUM] ์์ฑ๋ HTML์ CSP ํด์ ๋ถ์ผ์น ์ค๋ฅ ์์ #232
๐ก๏ธ Sentinel: [MEDIUM] ์์ฑ๋ HTML์ CSP ํด์ ๋ถ์ผ์น ์ค๋ฅ ์์ #232seonghobae wants to merge 3 commits into
Conversation
๐จ Severity: MEDIUM ๐ก Vulnerability: CSP `style-src` ํด์๊ฐ์ด ์ค์ `<style>` ํ๊ทธ ๋ด๋ถ ํ ์คํธ์ ์ผ์นํ์ง ์์, ๋ธ๋ผ์ฐ์ ๊ฐ CSS ๋ ๋๋ง์ ์ฐจ๋จํ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. ๐ฏ Impact: ์ธ๋ผ์ธ ์คํ์ผ์ด ๋ธ๋ผ์ฐ์ ์ Content Security Policy์ ์ํด ์ฐจ๋จ๋์ด UI ๋ ๋๋ง์ด ๊นจ์ง๋๋ค. ๐ง Fix: `main.kt`์์ `<style>` ํ๊ทธ ๋ด๋ถ์ Kotlin ํ ํ๋ฆฟ ๊ณต๋ฐฑ ๋ฐ ์ค๋ฐ๊ฟ์ ์ ๊ฑฐํ์ฌ ํด์ ๋์ ์๋ณธ๊ณผ ์ถ๋ ฅ๋๋ ๊ฐ์ด ์ ํํ ์ผ์นํ๋๋ก ์์ ํ์ต๋๋ค. โ Verification: `./gradlew test` ํต๊ณผ ๋ฐ ํตํฉ ํ ์คํธ์์ CSP ์ ๊ท์ ๋งค์น ํ ์คํธ ํต๊ณผ ํ์ธ.
|
๐ Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a ๐ emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this comment.
Pull request overview
Fixes a CSP regression in the generated index.html where the style-src SHA-256 hash did not match the actual <style> tag contents due to template-introduced whitespace, causing browsers to block the inline CSS in some cases.
Changes:
- Remove extra whitespace/newlines around injected CSS so the
<style>inner text matches the hashed string. - Add a regression test that recomputes the SHA-256 hash from the rendered
<style>content and compares it to the CSPstyle-srcvalue. - Document the incident and prevention guidance in
.jules/sentinel.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/main/kotlin/html4tree/main.kt | Ensures the <style> tag wraps cssContent without extra padding so CSP hash validation matches browser behavior. |
| src/test/kotlin/html4tree/MainTest.kt | Adds a regression test asserting the generated CSP style-src hash matches the actual inline style contents. |
| .jules/sentinel.md | Records the CSP hash mismatch issue and prevention guidance for future changes. |
๐ก Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val actualHash = match?.groupValues?.get(1) | ||
| val styleContentRegex = Regex("<style>([\\s\\S]*?)</style>") | ||
| 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))) |
๐จ Severity: MEDIUM ๐ก Vulnerability: CSP `style-src` ํด์๊ฐ์ด ์ค์ `<style>` ํ๊ทธ ๋ด๋ถ ํ ์คํธ์ ์ผ์นํ์ง ์์, ๋ธ๋ผ์ฐ์ ๊ฐ CSS ๋ ๋๋ง์ ์ฐจ๋จํ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. ๐ฏ Impact: ์ธ๋ผ์ธ ์คํ์ผ์ด ๋ธ๋ผ์ฐ์ ์ Content Security Policy์ ์ํด ์ฐจ๋จ๋์ด UI ๋ ๋๋ง์ด ๊นจ์ง๋๋ค. ๐ง Fix: `main.kt`์์ `<style>` ํ๊ทธ ๋ด๋ถ์ Kotlin ํ ํ๋ฆฟ ๊ณต๋ฐฑ ๋ฐ ์ค๋ฐ๊ฟ์ ์ ๊ฑฐํ์ฌ ํด์ ๋์ ์๋ณธ๊ณผ ์ถ๋ ฅ๋๋ ๊ฐ์ด ์ ํํ ์ผ์นํ๋๋ก ์์ ํ์ต๋๋ค. โ Verification: `./gradlew test` ํต๊ณผ ๋ฐ ํตํฉ ํ ์คํธ์์ CSP ์ ๊ท์ ๋งค์น ํ ์คํธ ํต๊ณผ ํ์ธ.
๐จ Severity: HIGH ๐ก Vulnerability: ๋๋ ํ ๋ฆฌ ํ์ ์ ์กฐ์ ๋๋ ํ ๋ฆฌ์ ์์นํ ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ์ถ์ ํ์ฌ ์๋๋ ์๋๋ฐ์ค๋ฅผ ๋ฒ์ด๋ ์์์ ๋๋ ํ ๋ฆฌ์ ์ธ๋ฑ์ค๋ฅผ ๊ธฐ๋กํ ์ ์๋ ์ทจ์ฝ์ (vuln-0002)๊ณผ ์์ ํ์ผ ์์ฑ ๊ณผ์ ์ TOCTOU(vuln-0001) ๋ ์ด์ค ์ปจ๋์ ์ทจ์ฝ์ ์ด ์์์ต๋๋ค. ๐ฏ Impact: ๊ณต๊ฒฉ์๊ฐ ์กฐ์ ๋๋ ํ ๋ฆฌ๋ฅผ ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ก ๋์ฒดํ์ฌ ํ์ฉ๋์ง ์์ ์์น์ ํ์ผ์ ์ฝ๊ณ ์ธ ์ ์๋ ์์ ํ์ผ ๋ฎ์ด์ฐ๊ธฐ๊ฐ ๋ฐ์ํฉ๋๋ค. ๐ง Fix: `has_symlink_ancestor` ํจ์๋ฅผ ์ถ๊ฐํ์ฌ ๋์ ๊ฒฝ๋ก์ ์กฐ์ ์ค ์ฌ๋ณผ๋ฆญ ๋งํฌ๊ฐ ์กด์ฌํ ๊ฒฝ์ฐ ์ธ๋ฑ์ค ์์ฑ ๋ฐ ํ ์ฝ์ ์ ์ฐจ๋จํ๊ณ , ์์ ํ์ผ ์ฐ๊ธฐ ๊ณผ์ (`Files.write`)์๋ `NOFOLLOW_LINKS` ์ต์ ์ ์ถ๊ฐํ์ต๋๋ค. CSP ํด์ ๋ถ์ผ์น ์์ ๋ ํฌํจํฉ๋๋ค. โ Verification: ์กฐ์ ๋๋ ํ ๋ฆฌ๊ฐ ์ฌ๋ณผ๋ฆญ ๋งํฌ์ธ ๊ฒฝ์ฐ `index.html` ์์ฑ์ ์ฐจ๋จํ๋ JUnit ํ ์คํธ ์ผ์ด์ค ์ถ๊ฐ ๋ฐ `./gradlew test` ํต๊ณผ ํ์ธ.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (2)
src/test/kotlin/html4tree/MainTest.kt:561
- If the CSP regex doesn't match,
actualHashbecomes null and the test failure is less direct (it will compare against null rather than clearly stating the CSP entry was missing). Adding an explicit assertion makes failures easier to diagnose.
val hashRegex = Regex("style-src '([^']+)'")
val match = hashRegex.find(content)
val actualHash = match?.groupValues?.get(1)
src/test/kotlin/html4tree/MainTest.kt:566
- If
<style>...</style>isn't found, the currentstyleContent!!causes an NPE that obscures the real assertion failure. Add an explicit assertion and avoid!!so the test fails with a clear message when the generated HTML changes.
val styleContentRegex = Regex("<style>([\\s\\S]*?)</style>")
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)
| val tempPath = Files.createTempFile(curr_dir.toPath(), ".index-", ".html") | ||
| try { | ||
| Files.write(tempPath, content.toByteArray(Charsets.UTF_8)) | ||
| Files.write(tempPath, content.toByteArray(Charsets.UTF_8), LinkOption.NOFOLLOW_LINKS) | ||
| Files.move(tempPath, indexPath, StandardCopyOption.REPLACE_EXISTING) |
| // โก Bolt Performance Optimization: Short-circuit OS stat calls (isDirectory/isSymbolicLink) | ||
| // by checking cheap in-memory string exclusion rules first | ||
| if(!it.name.startsWith(".") && it.name !in exclude && isDirectory(it) && !isSymbolicLink(it)) { | ||
| if(!it.name.startsWith(".") && it.name !in exclude && isDirectory(it) && !isSymbolicLink(it) && !has_symlink_ancestor(it)) { | ||
| val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key) |
| ## 2026-07-21 - ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ํตํ ๋ถ๋ชจ/์กฐ์ ๋๋ ํ ๋ฆฌ ๊ฒฝ๋ก ํ์ ์ทจ์ฝ์ ์ํ | ||
| **Vulnerability:** ๋๋ ํ ๋ฆฌ ํฌ๋กค๋ฌ๊ฐ ํ์ฌ ๊ฒฝ๋ก์ ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ง ํ์ธ()ํ๊ธฐ ๋๋ฌธ์, ์ค๊ฐ ๊ฒฝ๋ก(์กฐ์ ๋๋ ํ ๋ฆฌ)์ ํฌํจ๋ ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ๊ฑฐ์ณ์ ํ์ฉ๋์ง ์์ ์ธ๋ถ ๋๋ ํ ๋ฆฌ๋ก ์ํํ๊ณ ์ธ๋ฑ์ค๋ฅผ ์์ฑํ ์ ์๋ ์ทจ์ฝ์ ()๊ณผ ์์ ํ์ผ ์ค์ TOCTOU()์ด ๋ฐ๊ฒฌ๋์์ต๋๋ค. | ||
| **Learning:** ํ์ผ ๊ฒฝ๋ก์ ๋ง์ง๋ง ์ปดํฌ๋ํธ๋ง ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ๊ฒ์ฌํ๋ ๊ฒ์ ๋ถ์ถฉ๋ถํฉ๋๋ค. ์ ๋ ๊ฒฝ๋ก ์์ ์กฐ์ ๋๋ ํ ๋ฆฌ ์ค ํ๋๋ผ๋ ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ผ๋ฉด ์๋๋ฐ์ค๋ฅผ ๋ฒ์ด๋ ์ํ์ด ์์ผ๋ฏ๋ก, ํด๋น ๊ฒฝ๋ก์ ํ์ผ์ ์ฐ๋ ๊ฒ์ ์ฐจ๋จํด์ผ ํ๋ฉฐ, ์์ ํ์ผ ์์ฑ ๋ฐ ์ฐ๊ธฐ์๋ ์ต์ ์ ์ ์ฉํด ์ค์ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํด์ผ ํฉ๋๋ค. | ||
| **Prevention:** ๊ฒฝ๋ก ์ ๊ทํ ํ ๋ฃจํ๋ฅผ ํตํด ์กฐ์ ๋๋ ํ ๋ฆฌ ์ค ์ฌ๋ณผ๋ฆญ ๋งํฌ๊ฐ ์๋์ง ํ์ธํ๋ ๋ก์ง์ ์ถ๊ฐํ๊ณ , ํ์ผ ์์คํ IO ์คํผ๋ ์ด์ ()์๋ ๋งํฌ ์ถ์ ์ ์ ํํ์ญ์์ค. |
| fun has_symlink_ancestor(file: File): Boolean { | ||
| var path = file.toPath().toAbsolutePath().normalize() | ||
| while (path != null) { | ||
| if (Files.isSymbolicLink(path)) { | ||
| return true | ||
| } | ||
| path = path.parent | ||
| } | ||
| return false |
๐จ Severity: MEDIUM
๐ก Vulnerability: CSP
style-srcํด์๊ฐ์ด ์ค์ <style>ํ๊ทธ ๋ด๋ถ ํ ์คํธ์ ์ผ์นํ์ง ์์, ๋ธ๋ผ์ฐ์ ๊ฐ CSS ๋ ๋๋ง์ ์ฐจ๋จํ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.๐ฏ Impact: ์ธ๋ผ์ธ ์คํ์ผ์ด ๋ธ๋ผ์ฐ์ ์ Content Security Policy์ ์ํด ์ฐจ๋จ๋์ด UI ๋ ๋๋ง์ด ๊นจ์ง๋๋ค.
๐ง Fix:
main.kt์์<style>ํ๊ทธ ๋ด๋ถ์ Kotlin ํ ํ๋ฆฟ ๊ณต๋ฐฑ ๋ฐ ์ค๋ฐ๊ฟ์ ์ ๊ฑฐํ์ฌ ํด์ ๋์ ์๋ณธ๊ณผ ์ถ๋ ฅ๋๋ ๊ฐ์ด ์ ํํ ์ผ์นํ๋๋ก ์์ ํ์ต๋๋ค.โ Verification:
./gradlew testํต๊ณผ ๋ฐ ํตํฉ ํ ์คํธ์์ CSP ์ ๊ท์ ๋งค์น ํ ์คํธ ํต๊ณผ ํ์ธ.PR created automatically by Jules for task 3262535126628893184 started by @seonghobae