Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@
**Vulnerability:** 정적 HTML 생성 λ„κ΅¬μ—μ„œ 맀번 λ‹€λ₯Έ Nonceλ₯Ό λ™μ μœΌλ‘œ μƒμ„±ν•˜μ—¬ CSP에 μ μš©ν•˜λŠ” 것은, 캐싱 νš¨μœ¨μ„ μ €ν•˜μ‹œν‚¬ 뿐만 μ•„λ‹ˆλΌ 정적 배포 ν™˜κ²½(예: GitHub Pages λ“±)μ—μ„œ μ˜¬λ°”λ₯Έ λ³΄μ•ˆ μ •μ±… μˆ˜λ¦½μ„ λ°©ν•΄ν•  수 μžˆλŠ” μ•ˆν‹° νŒ¨ν„΄μž…λ‹ˆλ‹€.
**Learning:** μ •μ μœΌλ‘œ κ³ μ •λœ 인라인 μŠ€νƒ€μΌμ΄λ‚˜ μŠ€ν¬λ¦½νŠΈμ—λŠ” λ‚œμˆ˜ν™”λœ Nonce보닀 μ½˜ν…μΈ  자체의 ν•΄μ‹œ(SHA-256 λ“±)λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ•ˆμ „ν•˜κ³  μΌκ΄€λœ λ°©μ‹μž„μ„ λ°°μ› μŠ΅λ‹ˆλ‹€.
**Prevention:** μžλ™ μƒμ„±λ˜λŠ” 정적 HTML의 μ½˜ν…μΈ  λ³΄μ•ˆ μ •μ±…(CSP)μ—λŠ” `style-src 'sha256-<HASH>'` 방식을 μ μš©ν•˜κ³ , `<style>` νƒœκ·Έμ—μ„œ λΆˆν•„μš”ν•œ `nonce` 속성을 μ œκ±°ν•˜μ—¬ λΈŒλΌμš°μ €μ˜ 무결성 검증 κΈ°λŠ₯을 적극 ν™œμš©ν•˜μ‹­μ‹œμ˜€.

## 2026-07-27 - [html4tree] CSP Hash Mismatch due to Kotlin Multiline Strings
**Vulnerability:** λΈŒλΌμš°μ €κ°€ CSP(Content-Security-Policy) ν•΄μ‹œλ₯Ό 계산할 λ•Œ, 인라인 μŠ€ν¬λ¦½νŠΈμ™€ μŠ€νƒ€μΌμ˜ μ •ν™•ν•œ ν…μŠ€νŠΈμ— κΈ°λ°˜ν•©λ‹ˆλ‹€. Kotlin의 닀쀑 쀄 λ¬Έμžμ—΄ λ‚΄μ˜ λΆˆν•„μš”ν•œ 곡백과 μ€„λ°”κΏˆμœΌλ‘œ 인해 μƒμ„±λœ ν•΄μ‹œμ™€ λΈŒλΌμš°μ €κ°€ κ³„μ‚°ν•œ ν•΄μ‹œκ°€ λΆˆμΌμΉ˜ν•˜μ—¬, 결과적으둜 μŠ€νƒ€μΌ 적용이 μ‹€νŒ¨ν•˜κ±°λ‚˜ λ³΄μ•ˆ μ •μ±… 우회 κ°€λŠ₯성이 μƒκΉλ‹ˆλ‹€.
**Learning:** 정적 생성 λ„κ΅¬μ—μ„œ CSPλ₯Ό μœ„ν•΄ μ½˜ν…μΈ  ν•΄μ‹œλ₯Ό 계산할 λ•ŒλŠ”, 인젝트된 μ½˜ν…μΈ  μ£Όλ³€μ˜ 암묡적 μ—¬λ°±μ΄λ‚˜ λ“€μ—¬μ“°κΈ°κ°€ κ²°κ³Ό ν•΄μ‹œλ₯Ό λ¬΄νš¨ν™”ν•  수 μžˆμŒμ„ 인지해야 ν•©λ‹ˆλ‹€.
**Prevention:** μ½˜ν…μΈ  λ¬Έμžμ—΄μ— `.trimIndent()`λ₯Ό μ μš©ν•˜μ—¬ ν•΄μ‹± 전에 포맷을 μ •λ¦¬ν•˜κ³ , 암묡적 μ—¬λ°± 없이(예: `<style>${exactStyleContent}</style>`) HTML에 μΈμ νŠΈν•˜μ—¬ μ™„μ „ν•œ ν•΄μ‹œ 일치λ₯Ό 보μž₯ν•˜μ‹­μ‹œμ˜€.
Comment on lines +87 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ”’ Security & Privacy | 🟑 Minor | ⚑ Quick win

CSP 뢈일치의 영ν–₯κ³Ό κΈ°μ‘΄ nonce 지침을 μ •μ •ν•˜μ„Έμš”.

ν•΄μ‹œ λΆˆμΌμΉ˜λŠ” 일반적으둜 λΈŒλΌμš°μ €κ°€ μŠ€νƒ€μΌμ„ μ°¨λ‹¨ν•˜λŠ” λ¬Έμ œμ΄μ§€, λ³΄μ•ˆ μ •μ±… 우회λ₯Ό κ°€λŠ₯ν•˜κ²Œ ν•˜λŠ” λ¬Έμ œλŠ” μ•„λ‹™λ‹ˆλ‹€. λ˜ν•œ 24ν–‰μ˜ nonce 기반 지침이 ν˜„μž¬μ˜ hash 기반 κ΅¬ν˜„κ³Ό μΆ©λŒν•˜λ―€λ‘œ, κΈ°μ‘΄ ν•­λͺ©μ„ hash λ°©μ‹μœΌλ‘œ κ°±μ‹ ν•˜κ±°λ‚˜ 역사적 μ§€μΉ¨μž„μ„ λͺ…ν™•νžˆ ν‘œμ‹œν•˜μ„Έμš”.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/sentinel.md around lines 87 - 90, Update the CSP guidance in the
html4tree entry to state that hash mismatches typically cause browsers to block
styles, not enable a policy bypass. Revise the existing nonce-based guidance
around the referenced security instructions to match the current hash-based
implementation, or explicitly mark it as historical.

145 changes: 72 additions & 73 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,78 @@ fun main(args: Array<String>) = Html4tree().main(args)
internal data class FileIdentity(val key: Any?, val readable: Boolean)


private 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()

private val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8)))

private val css = """
<style>${cssContent}</style>
""".trimIndent()

internal fun read_file_identity(file: File): FileIdentity {
return try {
val attrs = Files.readAttributes(file.toPath(), BasicFileAttributes::class.java, LinkOption.NOFOLLOW_LINKS)
Expand Down Expand Up @@ -244,79 +316,6 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array

val exclude: Set<String> = excludeSet ?: process_ignore_file(curr_dir)

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;
}
"""

val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8)))

val css = """
<style>
${cssContent} </style>
"""

val index_top = """<!doctype html>
<html lang="ko">
<head>
Expand Down
Loading