Skip to content
Open
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
**Vulnerability:** `File(path).canonicalFile`λ₯Ό μ‚¬μš©ν•˜μ—¬ 심볼릭 링크 μ—¬λΆ€λ₯Ό κ²€μ‚¬ν•˜λ©΄, `canonicalFile` ν•¨μˆ˜ λ‚΄λΆ€μ—μ„œ 심볼릭 링크λ₯Ό 이미 λŒ€μƒ(target) 파일의 μ‹€μ œ 경둜둜 해석(resolve)ν•΄ 버리기 λ•Œλ¬Έμ—, 이후에 μ§„ν–‰λ˜λŠ” `Files.isDirectory(..., LinkOption.NOFOLLOW_LINKS)` λ“±μ˜ 심볼릭 링크 μ œν•œ 검사가 μ™„μ „νžˆ 무λ ₯ν™”λ˜λŠ” 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
**Learning:** `canonicalFile`은 λ³΄μ•ˆ 검사(경둜 μ‘°μž‘ λ“±)λ₯Ό μœ„ν•΄ μ ˆλŒ€ 경둜λ₯Ό 얻을 λ•Œ μœ μš©ν•  수 μžˆμ§€λ§Œ, 심볼릭 링크 자체의 νŠΉμ„±(심볼릭 링크인지 μ•„λ‹Œμ§€)을 보쑴해야 ν•˜λŠ” λ§₯λ½μ—μ„œλŠ” μ‚¬μš©ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€.
**Prevention:** 심볼릭 링크 μ—¬λΆ€λ₯Ό 검사해야 ν•˜κ±°λ‚˜ 심볼릭 링크 자체λ₯Ό μ œν•œν•΄μ•Ό ν•˜λŠ” κ²½μš°μ—λŠ” `canonicalFile` λŒ€μ‹  `absoluteFile.toPath().normalize().toFile()`κ³Ό 같이 심볼릭 링크λ₯Ό ν•΄μ„ν•˜μ§€ μ•Šκ³  경둜만 μ •κ·œν™”ν•˜λŠ” 방식을 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.
## 2024-05-27 - [Migrated from 'unsafe-inline' to Nonce-based CSP for Inline Styles]
**Vulnerability:** The static HTML generated by the tool used a strict Content-Security-Policy (CSP) that included `style-src 'unsafe-inline'`, which allows the execution of potentially malicious inline styles, increasing the risk of XSS attacks.
**Learning:** To adhere strictly to CSP best practices in static HTML generators, dynamic components like nonces should be generated at build/generation time. In Kotlin, this can be efficiently done using `SecureRandom()` to create a 16-byte nonce encoded in Base64.
**Prevention:** Avoid using `'unsafe-inline'` in `Content-Security-Policy` headers or meta tags. Instead, generate a unique nonce for each request or generated file and apply it to `<style>` tags and corresponding CSP directives. Replace inline `style="..."` attributes with predefined CSS classes.

## 2024-05-24 - [디렉토리 λͺ©λ‘μ„ ν†΅ν•œ 정보 λ…ΈμΆœ (Information Exposure)]
**Vulnerability:** 정적 HTML 디렉토리 μΈλ±μ„œ(`html4tree`)κ°€ λ―Όκ°ν•œ μ‹œμŠ€ν…œ 및 μ„€μ • 파일(`.git`, `.env`, `.ssh`, `.htpasswd` λ“±)을 ν¬ν•¨ν•˜μ—¬ λͺ¨λ“  파일과 디렉토리λ₯Ό λ¬΄λΆ„λ³„ν•˜κ²Œ λ‚˜μ—΄ν•˜μ—¬, μƒμ„±λœ HTML이 곡개적으둜 ν˜ΈμŠ€νŒ…λ  경우 μ‹¬κ°ν•œ 정보 λ…ΈμΆœλ‘œ μ΄μ–΄μ§ˆ 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
**Learning:** 디렉토리 ꡬ쑰λ₯Ό μžλ™ μƒμ„±ν•˜λŠ” λ„κ΅¬λŠ” λ°˜λ“œμ‹œ "기본적으둜 μ•ˆμ „ν•œ(secure by default)" 정책을 채택해야 ν•©λ‹ˆλ‹€. μ‚¬μš©μž 제곡 λ¬΄μ‹œ 파일(`.html4ignore`)μ—λ§Œ μ˜μ‘΄ν•˜λŠ” 것은 μ‚¬μš©μžκ°€ 섀정을 μžŠκ±°λ‚˜ μ–΄λ–€ 파일이 λ―Όκ°ν•œμ§€ λͺ¨λ₯Ό 수 있기 λ•Œλ¬Έμ— λΆˆμΆ©λΆ„ν•©λ‹ˆλ‹€.
Expand Down
25 changes: 20 additions & 5 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import java.io.File
import java.nio.file.Files
import java.nio.file.LinkOption
import java.nio.file.StandardCopyOption
import java.security.SecureRandom
import java.util.Base64
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.default
Expand Down Expand Up @@ -174,8 +176,12 @@ fun process_dir(curr_dir: File){

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

val nonceBytes = ByteArray(16)
SecureRandom().nextBytes(nonceBytes)
val nonce = Base64.getEncoder().encodeToString(nonceBytes)

val css = """
<style>
<style nonce="${nonce}">
ul {
list-style-type: none;
padding-left: 0;
Expand All @@ -193,6 +199,15 @@ fun process_dir(curr_dir: File){
outline: 2px solid #0366d6;
outline-offset: -2px;
}
.dir-link {
display: block;
width: 100%;
}
.empty-dir {
padding: 0.5rem;
color: #666;
font-style: italic;
}
@media (prefers-reduced-motion: reduce) {
a {
transition: none;
Expand All @@ -207,7 +222,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 @@ -216,7 +231,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="dir-link" href="./.." aria-label="μƒμœ„ λ””λ ‰ν† λ¦¬λ‘œ 이동"><span aria-hidden="true">&#x21B0;</span> ..</a></li>
"""

val index_middle = fun():String{
Expand All @@ -231,13 +246,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="dir-link" 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-dir">이 λ””λ ‰ν† λ¦¬λŠ” λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.</div></li>""")
l.append('\n')
}

Expand Down
2 changes: 1 addition & 1 deletion src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ 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';"))
assertTrue(htmlContent.contains("default-src 'none'; style-src 'nonce-"))
}

@Test
Expand Down
Loading