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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@
**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.
## 2026-07-04 - XSS in HTML Escaping Function
**Vulnerability:** The custom `escapeHtml` function did not escape forward slashes (`/`) and backslashes (`\`), allowing potential Cross-Site Scripting (XSS) if malicious payload relies on those unescaped characters.
**Learning:** Custom sanitization functions are prone to miss edge cases. When rolling a manual HTML escaper, it is critical to encode all sensitive structural characters including slashes, or preferably use a well-tested library.
**Prevention:** Always test custom escaping functions extensively and consider a security review. When modifying output sanitization logic, make sure the unit tests exhaustively cover special characters (e.g., `&<>\"'\`/\`).
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# λ³€κ²½ 사항

## [Unreleased]
### 좔가됨
- μƒμ„±λœ HTML 디렉토리 트리의 가독성을 κ°œμ„ ν•˜κΈ° μœ„ν•΄ μ‹œμŠ€ν…œ 폰트, λ°˜μ‘ν˜• μ΅œλŒ€ λ„ˆλΉ„ μ„€μ •, 그리고 닀크 λͺ¨λ“œ(`@media (prefers-color-scheme: dark)`) 지원을 μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. (UX κ°œμ„ )

### μˆ˜μ •λ¨
- μ΅œμƒμœ„ 디렉토리가 심볼릭 링크인 경우λ₯Ό μ˜¬λ°”λ₯΄κ²Œ κ±°λΆ€ν•˜λ„λ‘ 검증 λ‘œμ§μ„ μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. 이전에 `canonicalFile` 호좜둜 인해 λ°œμƒν•˜λŠ” 검증 우회 버그λ₯Ό ν•΄κ²°ν–ˆμŠ΅λ‹ˆλ‹€.
- λ³΄μ•ˆ: `escapeHtml` ν•¨μˆ˜μ—μ„œ `/` 및 `\` 문자λ₯Ό μΆ”κ°€λ‘œ μ΄μŠ€μΌ€μ΄ν”„ν•˜λ„λ‘ μˆ˜μ •ν•˜μ—¬ XSS 취약점을 μ™„ν™”ν–ˆμŠ΅λ‹ˆλ‹€.
26 changes: 25 additions & 1 deletion src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ fun main(args: Array<String>) = Html4tree().main(args)

fun go(topDir: String, maxLevel: Int) {
require(topDir.isNotBlank())
require(Files.isDirectory(File(topDir).toPath(), LinkOption.NOFOLLOW_LINKS)) { "Top directory must be an existing non-symlink directory" }
val top_dir = File(topDir).canonicalFile
require(Files.isDirectory(top_dir.toPath(), LinkOption.NOFOLLOW_LINKS)) { "Top directory must be an existing non-symlink directory" }

val ll = LinkedList()

Expand Down Expand Up @@ -55,6 +55,8 @@ fun String.escapeHtml(): String {
.replace("\"", "&quot;")
.replace("'", "&#x27;")
.replace("`", "&#x60;")
.replace("/", "&#x2F;")
.replace("\\", "&#x5C;")
}

fun String.urlEncodePath(): String {
Expand Down Expand Up @@ -134,6 +136,15 @@ fun process_dir(curr_dir: File){

val css = """
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
color: #24292e;
background-color: #ffffff;
}
ul {
list-style-type: none;
padding-left: 0;
Expand All @@ -150,6 +161,19 @@ fun process_dir(curr_dir: File){
outline: 2px solid #0366d6;
outline-offset: -2px;
}
@media (prefers-color-scheme: dark) {
body {
color: #c9d1d9;
background-color: #0d1117;
}
a {
color: #58a6ff;
}
a:hover, a:focus-visible {
background-color: #161b22;
outline: 2px solid #58a6ff;
}
}
</style>
"""

Expand Down
6 changes: 5 additions & 1 deletion src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ class MainTest {
assertEquals("&quot;", "\"".escapeHtml())
assertEquals("&#x27;", "'".escapeHtml())
assertEquals("&#x60;", "`".escapeHtml())
assertEquals("&amp;&lt;&gt;&quot;&#x27;&#x60;", "&<>\"'`".escapeHtml())
assertEquals("&#x2F;", "/".escapeHtml())
assertEquals("&#x5C;", "\\".escapeHtml())
assertEquals("&amp;&lt;&gt;&quot;&#x27;&#x60;&#x2F;&#x5C;", "&<>\"'`/\\".escapeHtml())
assertEquals("normal text", "normal text".escapeHtml())
}

Expand Down Expand Up @@ -160,6 +162,8 @@ class MainTest {
assertFalse(htmlContent.contains("test.ignore"))
assertTrue(htmlContent.contains("Content-Security-Policy"))
assertTrue(htmlContent.contains("default-src 'none'; style-src 'unsafe-inline';"))
assertTrue(htmlContent.contains("max-width: 800px;"))
assertTrue(htmlContent.contains("@media (prefers-color-scheme: dark)"))
}

@Test
Expand Down
Loading