Skip to content
Open
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 @@ -27,3 +27,8 @@
**Vulnerability:** `File(path).canonicalFile`λ₯Ό μ‚¬μš©ν•˜μ—¬ 심볼릭 링크 μ—¬λΆ€λ₯Ό κ²€μ‚¬ν•˜λ©΄, `canonicalFile` ν•¨μˆ˜ λ‚΄λΆ€μ—μ„œ 심볼릭 링크λ₯Ό 이미 λŒ€μƒ(target) 파일의 μ‹€μ œ 경둜둜 해석(resolve)ν•΄ 버리기 λ•Œλ¬Έμ—, 이후에 μ§„ν–‰λ˜λŠ” `Files.isDirectory(..., LinkOption.NOFOLLOW_LINKS)` λ“±μ˜ 심볼릭 링크 μ œν•œ 검사가 μ™„μ „νžˆ 무λ ₯ν™”λ˜λŠ” 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
**Learning:** `canonicalFile`은 λ³΄μ•ˆ 검사(경둜 μ‘°μž‘ λ“±)λ₯Ό μœ„ν•΄ μ ˆλŒ€ 경둜λ₯Ό 얻을 λ•Œ μœ μš©ν•  수 μžˆμ§€λ§Œ, 심볼릭 링크 자체의 νŠΉμ„±(심볼릭 링크인지 μ•„λ‹Œμ§€)을 보쑴해야 ν•˜λŠ” λ§₯λ½μ—μ„œλŠ” μ‚¬μš©ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€.
**Prevention:** 심볼릭 링크 μ—¬λΆ€λ₯Ό 검사해야 ν•˜κ±°λ‚˜ 심볼릭 링크 자체λ₯Ό μ œν•œν•΄μ•Ό ν•˜λŠ” κ²½μš°μ—λŠ” `canonicalFile` λŒ€μ‹  `absoluteFile.toPath().normalize().toFile()`κ³Ό 같이 심볼릭 링크λ₯Ό ν•΄μ„ν•˜μ§€ μ•Šκ³  경둜만 μ •κ·œν™”ν•˜λŠ” 방식을 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

## 2024-07-07 - [CSP κ°œμ„ : unsafe-inline 제거 및 nonce 적용]
**Vulnerability:** HTML 생성 μ‹œ `<meta http-equiv="Content-Security-Policy">`에 `style-src 'unsafe-inline'`이 μ„€μ •λ˜μ–΄ μžˆμ–΄, μ•…μ˜μ μΈ μ‚¬μš©μžκ°€ μŠ€νƒ€μΌ μΈμ μ…˜μ„ 톡해 XSS 곡격을 μˆ˜ν–‰ν•  μœ„ν—˜μ΄ μ‘΄μž¬ν–ˆμŠ΅λ‹ˆλ‹€.
**Learning:** `unsafe-inline`은 λΈŒλΌμš°μ €μ—μ„œ 인라인 μŠ€ν¬λ¦½νŠΈμ™€ μŠ€νƒ€μΌμ„ ν—ˆμš©ν•˜κΈ° λ•Œλ¬Έμ— λ³΄μ•ˆμƒ μ·¨μ•½ν•©λ‹ˆλ‹€. λ™μ μœΌλ‘œ μƒμ„±λ˜λŠ” HTML에 인라인 μŠ€νƒ€μΌμ„ μ μš©ν•΄μ•Ό ν•  경우, CSS 클래슀둜 λΆ„λ¦¬ν•˜κ³  μŠ€νƒ€μΌ νƒœκ·Έμ— λ™μ μœΌλ‘œ μƒμ„±λœ `nonce`λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 더 μ•ˆμ „ν•˜λ‹€λŠ” 것을 λ°°μ› μŠ΅λ‹ˆλ‹€.
**Prevention:** ν–₯ν›„ HTML 생성 μ‹œ 인라인 μŠ€νƒ€μΌ 속성(`style="..."`)의 μ‚¬μš©μ„ μ§€μ–‘ν•˜κ³ , CSS ν΄λž˜μŠ€μ™€ `nonce` 기반의 CSPλ₯Ό μ—„κ²©ν•˜κ²Œ μ μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.
20 changes: 15 additions & 5 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,23 @@ fun write_index_file(curr_dir: File, content: String) {
fun process_dir(curr_dir: File){

val exclude: Set<String> = process_ignore_file(curr_dir)
val nonce = java.util.UUID.randomUUID().toString().replace("-", "")

val css = """
<style>
<style nonce="$nonce">
ul {
list-style-type: none;
padding-left: 0;
}
.link-item {
display: block;
width: 100%;
}
.empty-msg {
padding: 0.5rem;
color: #666;
font-style: italic;
}
a {
padding: 0.5rem;
text-decoration: none;
Expand All @@ -182,7 +192,7 @@ fun process_dir(curr_dir: File){
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- λ³΄μ•ˆ ν–₯상: 인라인 슀크립트 μ‹€ν–‰ λ°©μ§€ -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'nonce-$nonce';">
<title>${curr_dir.getName().escapeHtml()}</title>
${css}
</head>
Expand All @@ -191,7 +201,7 @@ fun process_dir(curr_dir: File){
<h1>${curr_dir.getName().escapeHtml()}</h1>
<nav aria-label="Directory listing">
<ul>
<li><a style="display:block; width:100%" href="./.." aria-label="μƒμœ„ λ””λ ‰ν† λ¦¬λ‘œ 이동"><span aria-hidden="true">&#x21B0;</span> ..</a></li>
<li><a class="link-item" href="./.." aria-label="μƒμœ„ λ””λ ‰ν† λ¦¬λ‘œ 이동"><span aria-hidden="true">&#x21B0;</span> ..</a></li>
"""

val index_middle = fun():String{
Expand All @@ -206,13 +216,13 @@ fun process_dir(curr_dir: File){
val encodedHref = if (isLinkedDirectory) { "./${fileName.urlEncodePath()}/" } else { "./${fileName.urlEncodePath()}" }
val ariaLabel = "${fileName} ${if (isLinkedDirectory) { "디렉토리" } else { "파일" }}".escapeHtml()
val icon = if (isLinkedDirectory) { "&#128193;" } else { "&rtrif;" }
l.append(""" <li><a style="display:block; width:100%" href="${encodedHref}" aria-label="${ariaLabel}"><span aria-hidden="true">${icon}</span> ${fileName.escapeHtml()}</a></li>""")
l.append(""" <li><a class="link-item" href="${encodedHref}" aria-label="${ariaLabel}"><span aria-hidden="true">${icon}</span> ${fileName.escapeHtml()}</a></li>""")
l.append('\n')
}
}

if(l.isEmpty()){
l.append(""" <li><div style="padding: 0.5rem; color: #666; font-style: italic;">이 λ””λ ‰ν† λ¦¬λŠ” λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.</div></li>""")
l.append(""" <li><div class="empty-msg">이 λ””λ ‰ν† λ¦¬λŠ” λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.</div></li>""")
l.append('\n')
}

Expand Down
9 changes: 8 additions & 1 deletion src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.assertNotNull

class MainTest {
private lateinit var tempDir: File
Expand Down Expand Up @@ -160,7 +161,13 @@ class MainTest {
assertTrue(htmlContent.contains("&#128193;"))
assertFalse(htmlContent.contains("test.ignore"))
assertTrue(htmlContent.contains("Content-Security-Policy"))
assertTrue(htmlContent.contains("default-src 'none'; style-src 'unsafe-inline';"))
assertFalse(htmlContent.contains("'unsafe-inline'"))
val cspRegex = "style-src 'nonce-([a-f0-9-]{32,36})'".toRegex()
val matchResult = cspRegex.find(htmlContent)
assertNotNull(matchResult, "CSP nonce should be generated")
val nonce = matchResult.groups[1]?.value ?: ""
assertTrue(htmlContent.contains("<style nonce=\"$nonce\">"), "Style tag should use the generated nonce")
assertTrue(htmlContent.contains("class=\"link-item\""))
}

@Test
Expand Down
Loading