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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@
**Vulnerability:** 정적 HTML 생성 λ„κ΅¬μ—μ„œ 맀번 λ‹€λ₯Έ Nonceλ₯Ό λ™μ μœΌλ‘œ μƒμ„±ν•˜μ—¬ CSP에 μ μš©ν•˜λŠ” 것은, 캐싱 νš¨μœ¨μ„ μ €ν•˜μ‹œν‚¬ 뿐만 μ•„λ‹ˆλΌ 정적 배포 ν™˜κ²½(예: GitHub Pages λ“±)μ—μ„œ μ˜¬λ°”λ₯Έ λ³΄μ•ˆ μ •μ±… μˆ˜λ¦½μ„ λ°©ν•΄ν•  수 μžˆλŠ” μ•ˆν‹° νŒ¨ν„΄μž…λ‹ˆλ‹€.
**Learning:** μ •μ μœΌλ‘œ κ³ μ •λœ 인라인 μŠ€νƒ€μΌμ΄λ‚˜ μŠ€ν¬λ¦½νŠΈμ—λŠ” λ‚œμˆ˜ν™”λœ Nonce보닀 μ½˜ν…μΈ  자체의 ν•΄μ‹œ(SHA-256 λ“±)λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ•ˆμ „ν•˜κ³  μΌκ΄€λœ λ°©μ‹μž„μ„ λ°°μ› μŠ΅λ‹ˆλ‹€.
**Prevention:** μžλ™ μƒμ„±λ˜λŠ” 정적 HTML의 μ½˜ν…μΈ  λ³΄μ•ˆ μ •μ±…(CSP)μ—λŠ” `style-src 'sha256-<HASH>'` 방식을 μ μš©ν•˜κ³ , `<style>` νƒœκ·Έμ—μ„œ λΆˆν•„μš”ν•œ `nonce` 속성을 μ œκ±°ν•˜μ—¬ λΈŒλΌμš°μ €μ˜ 무결성 검증 κΈ°λŠ₯을 적극 ν™œμš©ν•˜μ‹­μ‹œμ˜€.
## 2026-08-01 - [html4tree] CSP ν•΄μ‹œ 뢈일치 λ°©μ§€λ₯Ό μœ„ν•œ 인라인 μŠ€νƒ€μΌ μ •μ œ 및 μ„±λŠ₯ μ΅œμ ν™”
**Vulnerability:** Kotlin 닀쀑 쀄 λ¬Έμžμ—΄(multiline string)을 μ‚¬μš©ν•˜μ—¬ 인라인 μŠ€νƒ€μΌμ„ μ£Όμž…ν•  λ•Œ, μ˜λ„μΉ˜ μ•Šμ€ κ³΅λ°±μ΄λ‚˜ μ€„λ°”κΏˆμ΄ ν¬ν•¨λ˜λ©΄ λΈŒλΌμš°μ €κ°€ κ³„μ‚°ν•˜λŠ” CSP ν•΄μ‹œκ°’κ³Ό μƒμ„±λœ 메타 νƒœκ·Έμ˜ ν•΄μ‹œκ°’μ΄ μΌμΉ˜ν•˜μ§€ μ•Šμ•„(CSP Hash Mismatch), λ³΄μ•ˆ 정책이 μ˜¬λ°”λ₯΄κ²Œ μ μš©λ˜μ§€ μ•Šκ±°λ‚˜ μŠ€νƒ€μΌμ΄ μ°¨λ‹¨λ˜λŠ” λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€.
**Learning:** μ½˜ν…μΈ  λ³΄μ•ˆ μ •μ±…(CSP) ν•΄μ‹œλŠ” λŒ€μƒ μ½˜ν…μΈ μ˜ μ •ν™•ν•œ λ‚΄λΆ€ ν…μŠ€νŠΈ(inner text)λ₯Ό 기반으둜 κ³„μ‚°λ©λ‹ˆλ‹€. λ”°λΌμ„œ ν…œν”Œλ¦Ώμ— λ¬Έμžμ—΄μ„ μ‚½μž…ν•  λ•Œ μ•”μ‹œμ μΈ νŒ¨λ”©μ΄λ‚˜ 여백이 ν¬ν•¨λ˜μ§€ μ•Šλ„λ‘ μ£Όμ˜ν•΄μ•Ό ν•˜λ©°, 맀번 ν•¨μˆ˜ 호좜 μ‹œ ν•΄μ‹œλ₯Ό κ³„μ‚°ν•˜λŠ” 것은 μ„±λŠ₯ μ €ν•˜λ₯Ό μΌμœΌν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.
**Prevention:** ν•΄μ‹œ 계산 μ „ λŒ€μƒ λ¬Έμžμ—΄μ— `.trimIndent()`λ₯Ό μ μš©ν•˜μ—¬ 곡백을 μ œκ±°ν•˜κ³ , HTML에 μ£Όμž…ν•  λ•Œ `<style>${exactStyleContent}</style>`와 같이 곡백 없이 μ£Όμž…ν•˜μ‹­μ‹œμ˜€. λ˜ν•œ κ³ μ •λœ 정적 λ¬Έμžμ—΄κ³Ό ν•΄μ‹œ 계산은 `private val` ν˜•νƒœμ˜ μ΅œμƒμœ„ μƒμˆ˜λ‘œ λŒμ–΄μ˜¬λ € 쀑볡 계산과 λ©”λͺ¨λ¦¬ 할당을 λ°©μ§€ν•˜μ‹­μ‹œμ˜€.
141 changes: 69 additions & 72 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,74 @@ import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.types.int

private val exactStyleContent = """
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(exactStyleContent.toByteArray(Charsets.UTF_8)))

class Html4tree : CliktCommand() {
val maxLevel:Int by option(help="Number of levels deep for which to generate an index.html file", hidden = false).int().default(-1)
val topDir: String by argument(help="Top directory to crawl")
Expand Down Expand Up @@ -244,78 +312,7 @@ 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 css = """<style>${exactStyleContent}</style>"""

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