diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 6c61284..120cc6d 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -22,3 +22,8 @@ **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-03 - [Symlink Resolution Vulnerability Bypass] +**Vulnerability:** The top directory path evaluation used `canonicalFile`, which resolves symlinks. This bypassed the security check `Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)` and allowed paths containing symlinks to be processed unexpectedly. +**Learning:** `canonicalFile` strictly resolves all symlinks in the path. Using it before checking for symlinks defeats the purpose of the security check as the target is evaluated directly. +**Prevention:** Use `absoluteFile.normalize()` instead of `canonicalFile` when the goal is to evaluate the path logic (like `.` and `..`) without traversing symlinks. This maintains the true file type to properly enforce `LinkOption.NOFOLLOW_LINKS`. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 2e2809f..a0d3deb 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -23,7 +23,9 @@ fun main(args: Array) = Html4tree().main(args) fun go(topDir: String, maxLevel: Int) { require(topDir.isNotBlank()) - val top_dir = File(topDir).canonicalFile + // 보안 수정: canonicalFile은 심볼릭 링크를 미리 해석하여 NOFOLLOW_LINKS 검사를 우회할 수 있습니다. + // absoluteFile.normalize()를 사용하여 심볼릭 링크 해석 없이 경로만 정규화합니다. + val top_dir = File(topDir).absoluteFile.normalize() require(Files.isDirectory(top_dir.toPath(), LinkOption.NOFOLLOW_LINKS)) { "Top directory must be an existing non-symlink directory" } val ll = LinkedList() diff --git a/src/test/kotlin/html4tree/GoNormalizeTest.kt b/src/test/kotlin/html4tree/GoNormalizeTest.kt new file mode 100644 index 0000000..1200de6 --- /dev/null +++ b/src/test/kotlin/html4tree/GoNormalizeTest.kt @@ -0,0 +1,22 @@ +package html4tree + +import org.junit.Test +import java.io.File +import java.nio.file.Files +import kotlin.test.assertTrue + +class GoNormalizeTest { + @Test + fun testGoNormalize() { + val tempDir = Files.createTempDirectory("html4tree-test-").toFile() + try { + // Test that normalize() logic works without errors by using a path ending with /.. + val subdir = File(tempDir, "subdir") + subdir.mkdir() + go(subdir.absolutePath + "/..", -1) + assertTrue(File(tempDir, "index.html").exists()) + } finally { + tempDir.deleteRecursively() + } + } +}