Skip to content

๐Ÿ›ก๏ธ Sentinel: [MEDIUM] ์ƒ์„ฑ๋œ HTML์˜ CSP ํ•ด์‹œ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜ ์ˆ˜์ •#232

Open
seonghobae wants to merge 3 commits into
masterfrom
sentinel-fix-csp-hash-3262535126628893184
Open

๐Ÿ›ก๏ธ Sentinel: [MEDIUM] ์ƒ์„ฑ๋œ HTML์˜ CSP ํ•ด์‹œ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜ ์ˆ˜์ •#232
seonghobae wants to merge 3 commits into
masterfrom
sentinel-fix-csp-hash-3262535126628893184

Conversation

@seonghobae

Copy link
Copy Markdown
Collaborator

๐Ÿšจ 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

๐Ÿšจ Severity: MEDIUM
๐Ÿ’ก Vulnerability: CSP `style-src` ํ•ด์‹œ๊ฐ’์ด ์‹ค์ œ `<style>` ํƒœ๊ทธ ๋‚ด๋ถ€ ํ…์ŠคํŠธ์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ CSS ๋ Œ๋”๋ง์„ ์ฐจ๋‹จํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
๐ŸŽฏ Impact: ์ธ๋ผ์ธ ์Šคํƒ€์ผ์ด ๋ธŒ๋ผ์šฐ์ €์˜ Content Security Policy์— ์˜ํ•ด ์ฐจ๋‹จ๋˜์–ด UI ๋ Œ๋”๋ง์ด ๊นจ์ง‘๋‹ˆ๋‹ค.
๐Ÿ”ง Fix: `main.kt`์—์„œ `<style>` ํƒœ๊ทธ ๋‚ด๋ถ€์˜ Kotlin ํ…œํ”Œ๋ฆฟ ๊ณต๋ฐฑ ๋ฐ ์ค„๋ฐ”๊ฟˆ์„ ์ œ๊ฑฐํ•˜์—ฌ ํ•ด์‹œ ๋Œ€์ƒ ์›๋ณธ๊ณผ ์ถœ๋ ฅ๋˜๋Š” ๊ฐ’์ด ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
โœ… Verification: `./gradlew test` ํ†ต๊ณผ ๋ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—์„œ CSP ์ •๊ทœ์‹ ๋งค์น˜ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ํ™•์ธ.
Copilot AI review requested due to automatic review settings July 21, 2026 03:55
@google-labs-jules

Copy link
Copy Markdown

๐Ÿ‘‹ 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CSP style-src value.
  • 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.

Comment on lines +543 to +547
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` ํ†ต๊ณผ ํ™•์ธ.
Copilot AI review requested due to automatic review settings July 21, 2026 05:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, actualHash becomes 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 current styleContent!! 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)

Comment on lines 248 to 251
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)
Comment on lines 106 to 109
// โšก 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)
Comment thread .jules/sentinel.md
Comment on lines +90 to +93
## 2026-07-21 - ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ํ†ตํ•œ ๋ถ€๋ชจ/์กฐ์ƒ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ ํƒ์ƒ‰ ์ทจ์•ฝ์  ์™„ํ™”
**Vulnerability:** ๋””๋ ‰ํ† ๋ฆฌ ํฌ๋กค๋Ÿฌ๊ฐ€ ํ˜„์žฌ ๊ฒฝ๋กœ์˜ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๋งŒ ํ™•์ธ()ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ค‘๊ฐ„ ๊ฒฝ๋กœ(์กฐ์ƒ ๋””๋ ‰ํ† ๋ฆฌ)์— ํฌํ•จ๋œ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ๊ฑฐ์ณ์„œ ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์™ธ๋ถ€ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ˆœํšŒํ•˜๊ณ  ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์ทจ์•ฝ์ ()๊ณผ ์ž„์‹œ ํŒŒ์ผ ์Šค์™‘ TOCTOU()์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
**Learning:** ํŒŒ์ผ ๊ฒฝ๋กœ์˜ ๋งˆ์ง€๋ง‰ ์ปดํฌ๋„ŒํŠธ๋งŒ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ๊ฒƒ์€ ๋ถˆ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ์ ˆ๋Œ€ ๊ฒฝ๋กœ ์ƒ์˜ ์กฐ์ƒ ๋””๋ ‰ํ† ๋ฆฌ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๋ผ๋ฉด ์ƒŒ๋“œ๋ฐ•์Šค๋ฅผ ๋ฒ—์–ด๋‚  ์œ„ํ—˜์ด ์žˆ์œผ๋ฏ€๋กœ, ํ•ด๋‹น ๊ฒฝ๋กœ์— ํŒŒ์ผ์„ ์“ฐ๋Š” ๊ฒƒ์„ ์ฐจ๋‹จํ•ด์•ผ ํ•˜๋ฉฐ, ์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ์“ฐ๊ธฐ์—๋„ ์˜ต์…˜์„ ์ ์šฉํ•ด ์Šค์™‘ ๊ณต๊ฒฉ์„ ๋ฐฉ์–ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
**Prevention:** ๊ฒฝ๋กœ ์ •๊ทœํ™” ํ›„ ๋ฃจํ”„๋ฅผ ํ†ตํ•ด ์กฐ์ƒ ๋””๋ ‰ํ† ๋ฆฌ ์ค‘ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ณ , ํŒŒ์ผ ์‹œ์Šคํ…œ IO ์˜คํผ๋ ˆ์ด์…˜()์—๋„ ๋งํฌ ์ถ”์ ์„ ์ œํ•œํ•˜์‹ญ์‹œ์˜ค.
Comment on lines +40 to +48
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants