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
1 change: 1 addition & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
**Vulnerability:** Defense in Depth (CSP Missing)
**Learning:** Even when inputs are properly escaped, statically generated HTML that displays file/directory structures should implement a Content Security Policy (CSP) to provide an extra layer of defense against potential XSS bypasses.
**Prevention:** Include a strict CSP meta tag (e.g., `default-src 'none'; style-src 'unsafe-inline';`) in auto-generated HTML headers when external scripts or resources are not required.
## 2024-06-29 - [html4tree] Strict CSP Nonce Implementation\n**Vulnerability:** Weak Content Security Policy (CSP)\n**Learning:** The previous CSP allowed `style-src 'unsafe-inline'`, which is generally discouraged as it can weaken defense-in-depth against XSS attacks. The html4tree output relied on `style="..."` attributes.\n**Prevention:** A secure random `nonce` is now generated per execution using `java.security.SecureRandom` and applied to the `<style>` block. Inline styles were refactored into CSS classes (`.item-link`, `.empty-msg`) to eliminate the need for `unsafe-inline`.
26 changes: 21 additions & 5 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

package html4tree

import java.io.File
import java.security.SecureRandom
import java.util.Base64
import java.nio.file.Files
import java.nio.file.LinkOption
import java.nio.file.StandardCopyOption
Expand Down Expand Up @@ -132,8 +135,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 @@ -150,6 +157,15 @@ fun process_dir(curr_dir: File){
outline: 2px solid #0366d6;
outline-offset: -2px;
}
.item-link {
display: block;
width: 100%;
}
.empty-msg {
padding: 0.5rem;
color: #666;
font-style: italic;
}
</style>
"""

Expand All @@ -159,7 +175,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 @@ -168,7 +184,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="item-link" href="./.." aria-label="μƒμœ„ λ””λ ‰ν† λ¦¬λ‘œ 이동"><span aria-hidden="true">&#x21B0;</span> ..</a></li>
"""

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

Expand Down
3 changes: 2 additions & 1 deletion src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ 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-"))
assertTrue(htmlContent.contains("class=\"item-link\""))
}

@Test
Expand Down
Loading