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 @@ -83,3 +83,7 @@
**Vulnerability:** 정적 HTML 생성 λ„κ΅¬μ—μ„œ 맀번 λ‹€λ₯Έ Nonceλ₯Ό λ™μ μœΌλ‘œ μƒμ„±ν•˜μ—¬ CSP에 μ μš©ν•˜λŠ” 것은, 캐싱 νš¨μœ¨μ„ μ €ν•˜μ‹œν‚¬ 뿐만 μ•„λ‹ˆλΌ 정적 배포 ν™˜κ²½(예: GitHub Pages λ“±)μ—μ„œ μ˜¬λ°”λ₯Έ λ³΄μ•ˆ μ •μ±… μˆ˜λ¦½μ„ λ°©ν•΄ν•  수 μžˆλŠ” μ•ˆν‹° νŒ¨ν„΄μž…λ‹ˆλ‹€.
**Learning:** μ •μ μœΌλ‘œ κ³ μ •λœ 인라인 μŠ€νƒ€μΌμ΄λ‚˜ μŠ€ν¬λ¦½νŠΈμ—λŠ” λ‚œμˆ˜ν™”λœ Nonce보닀 μ½˜ν…μΈ  자체의 ν•΄μ‹œ(SHA-256 λ“±)λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ•ˆμ „ν•˜κ³  μΌκ΄€λœ λ°©μ‹μž„μ„ λ°°μ› μŠ΅λ‹ˆλ‹€.
**Prevention:** μžλ™ μƒμ„±λ˜λŠ” 정적 HTML의 μ½˜ν…μΈ  λ³΄μ•ˆ μ •μ±…(CSP)μ—λŠ” `style-src 'sha256-<HASH>'` 방식을 μ μš©ν•˜κ³ , `<style>` νƒœκ·Έμ—μ„œ λΆˆν•„μš”ν•œ `nonce` 속성을 μ œκ±°ν•˜μ—¬ λΈŒλΌμš°μ €μ˜ 무결성 검증 κΈ°λŠ₯을 적극 ν™œμš©ν•˜μ‹­μ‹œμ˜€.
## 2026-07-17 - [Local CLI Path Traversal False Positive Prevention]
**Vulnerability:** A local CLI tool (html4tree) incorrectly blocked relative path traversal using `..` as a security feature.
**Learning:** Preventing `..` in local CLI tools creates usability bugs, as relative paths are an expected operating system standard. Path traversal logic should distinguish between web inputs and local administrative commands.
**Prevention:** Do not enforce web-based path traversal protections (like banning `..`) on raw local filesystem paths unless the path escapes an explicitly defined, sandboxed root directory.
12 changes: 10 additions & 2 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ internal fun read_file_identity(file: File): FileIdentity {

fun go(topDir: String, maxLevel: Int) {
require(topDir.isNotBlank())
require(!topDir.contains("..")) { "Path traversal sequences are not allowed." }
// λ³΄μ•ˆ μˆ˜μ •: symlink 검사λ₯Ό μš°νšŒν•˜λŠ” canonicalFile λŒ€μ‹  absoluteFile을 μ‚¬μš©
// canonicalFile은 symlinkλ₯Ό λŒ€μƒ 경둜둜 ν•΄μ„ν•˜μ—¬ μ΄μ–΄μ§€λŠ” NOFOLLOW_LINKS 검사λ₯Ό 무λ ₯ν™”ν•©λ‹ˆλ‹€.
val top_dir = File(topDir).absoluteFile.toPath().normalize().toFile()
Expand Down Expand Up @@ -219,10 +218,19 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
val defaultSensitiveFiles = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json")
files_to_exclude.addAll(defaultSensitiveFiles)

// λ³΄μ•ˆ ν–₯상: .env, .git λ“± λ―Όκ°ν•œ 정보가 포함될 수 μžˆλŠ” μˆ¨κΉ€ 파일(.으둜 μ‹œμž‘ν•˜λŠ” λͺ¨λ“  ν•­λͺ©)을 기본적으둜 λ…ΈμΆœν•˜μ§€ μ•Šλ„λ‘ μ œμ™Έ (정보 λ…ΈμΆœ λ°©μ§€)
val sensitiveExtensions = listOf(".pem", ".key", ".sqlite", ".log", ".bak", ".db", ".sql")

// λ³΄μ•ˆ ν–₯상: .env, .git λ“± λ―Όκ°ν•œ 정보가 포함될 수 μžˆλŠ” μˆ¨κΉ€ 파일(.으둜 μ‹œμž‘ν•˜λŠ” λͺ¨λ“  ν•­λͺ©) 및 λ―Όκ°ν•œ ν™•μž₯자(.pem, .key λ“±)λ₯Ό 기본적으둜 μ œμ™Έ (정보 λ…ΈμΆœ λ°©μ§€)
(dirFilesNames ?: curr_dir.list())?.forEach {
if (it.startsWith(".")) {
files_to_exclude.add(it)
} else {
for (ext in sensitiveExtensions) {
if (it.endsWith(ext, ignoreCase = true)) {
files_to_exclude.add(it)
break
}
}
}
}

Expand Down
41 changes: 37 additions & 4 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,17 @@ class MainTest {
}

@Test
fun testGoRejectsRelativePathTraversal() {
assertFailsWith<IllegalArgumentException> {
go("../../../etc/passwd", -1)
}
fun testGoAcceptsRelativePathTraversal() {
val subdir = File(tempDir, "subdir")
subdir.mkdir()
val fileInSubdir = File(subdir, "test.txt")
fileInSubdir.writeText("test")

// Use relative path with .. to navigate back and forth
val relativePath = "${subdir.absolutePath}/../${subdir.name}"
go(relativePath, -1)

assertTrue(File(subdir, "index.html").exists())
}

@Test
Expand Down Expand Up @@ -514,6 +521,32 @@ class MainTest {
assertTrue(excluded.contains("index.html"))
}

@Test
fun testProcessIgnoreFileSensitiveExtensions() {
File(tempDir, "private.key").createNewFile()
File(tempDir, "cert.pem").createNewFile()
File(tempDir, "database.sqlite").createNewFile()
File(tempDir, "app.log").createNewFile()
File(tempDir, "backup.bak").createNewFile()
File(tempDir, "data.db").createNewFile()
File(tempDir, "dump.sql").createNewFile()
File(tempDir, "safe.txt").createNewFile()
File(tempDir, "public.html").createNewFile()

val excluded = process_ignore_file(tempDir)

assertTrue(excluded.contains("private.key"))
assertTrue(excluded.contains("cert.pem"))
assertTrue(excluded.contains("database.sqlite"))
assertTrue(excluded.contains("app.log"))
assertTrue(excluded.contains("backup.bak"))
assertTrue(excluded.contains("data.db"))
assertTrue(excluded.contains("dump.sql"))

assertFalse(excluded.contains("safe.txt"))
assertFalse(excluded.contains("public.html"))
}

@Test
fun testProcessDirItEqualsCurrDir() {
File(tempDir, "tempDir").mkdir()
Expand Down
Loading