diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a21dad7..bc2ec38 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -45,3 +45,7 @@ **Vulnerability:** The `.html4ignore` parser still allowed excessively large files and root-directory crawls could generate unbounded filesystem output. **Learning:** Local CLI configuration inputs and traversal roots need explicit resource ceilings, not only syntactic validation. **Prevention:** Limit `.html4ignore` file size, parsed line count, compiled pattern count, and regex length; reject filesystem root traversal using `File.parentFile != null`. +## 2024-06-29 - [html4tree] App Crashing on Unreadable Files/Unwritable Directories +**Vulnerability:** DoS (Denial of Service) via Permissions +**Learning:** Application blindly reading `.html4ignore` or attempting to create `index.html` without verifying `canRead()` or catching `IOException`/`AccessDeniedException` allows a user/process with sufficient privileges to create directories/files with restricted permissions, crashing the entire tree traversal crawler. +**Prevention:** Always verify `canRead()` on configuration files before parsing them, and wrap I/O operations (like file writes) in `try-catch` blocks for `IOException` to ensure the crawler skips restricted areas gracefully rather than terminating. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 936c2b7..6263a91 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -120,8 +120,9 @@ fun process_ignore_file(curr_dir: File): Set { val files_to_exclude = mutableSetOf() // 보안 향상: .html4ignore 파일이 일반 파일인지 확인하고, 심볼릭 링크인 경우 무시하여 DoS 및 경로 조작을 방지합니다. + // 보안 향상: 권한 기반 서비스 거부(DoS) 방지를 위해 읽기 권한을 확인합니다. // 보안 향상: 파일 크기(1MB 제한) 및 줄 수(1000줄), 정규식 길이(100자)를 제한하여 ReDoS 및 메모리 고갈(OOM) 방지 - if(ignore_file.isFile && !Files.isSymbolicLink(ignore_file.toPath()) && ignore_file.length() <= 1048576){ + if(ignore_file.isFile && !Files.isSymbolicLink(ignore_file.toPath()) && ignore_file.canRead() && ignore_file.length() <= 1048576){ val ignored_regexes = mutableListOf() ignore_file.useLines { lines -> @@ -160,12 +161,16 @@ fun process_ignore_file(curr_dir: File): Set { fun write_index_file(curr_dir: File, content: String) { val indexPath = curr_dir.toPath().resolve("index.html") - val tempPath = Files.createTempFile(curr_dir.toPath(), ".index-", ".html") try { - Files.write(tempPath, content.toByteArray(Charsets.UTF_8)) - Files.move(tempPath, indexPath, StandardCopyOption.REPLACE_EXISTING) - } finally { - Files.deleteIfExists(tempPath) + val tempPath = Files.createTempFile(curr_dir.toPath(), ".index-", ".html") + try { + Files.write(tempPath, content.toByteArray(Charsets.UTF_8)) + Files.move(tempPath, indexPath, StandardCopyOption.REPLACE_EXISTING) + } finally { + Files.deleteIfExists(tempPath) + } + } catch (e: java.io.IOException) { + // 보안 향상: 쓰기 권한이 없는 디렉토리에서 크래시(가용성 저하)가 발생하지 않도록 예외를 잡아서 무시합니다. } } diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 9988cee..895edc2 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -172,9 +172,9 @@ class MainTest { indexDir.mkdir() File(indexDir, "occupant.txt").writeText("keep") - assertFailsWith { - write_index_file(tempDir, "content") - } + // Since we now catch IOException and AccessDeniedException in write_index_file, + // it should no longer throw an exception. + write_index_file(tempDir, "content") assertTrue(indexDir.isDirectory) assertEquals("keep", File(indexDir, "occupant.txt").readText()) @@ -453,4 +453,39 @@ class MainTest { // Line 1001 should be ignored due to line limit assertFalse(excluded.contains("test.txt1001")) } + + @Test + fun testProcessIgnoreFileUnreadable() { + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText(".*\\.txt\n") + File(tempDir, "test.txt").createNewFile() + + try { + Assume.assumeTrue(ignoreFile.setReadable(false, false)) + val excluded = process_ignore_file(tempDir) + // unreadable ignore file is ignored, so the file is not excluded + assertFalse(excluded.contains("test.txt")) + assertTrue(excluded.contains("index.html")) + } finally { + ignoreFile.setReadable(true, false) + } + } + + @Test + fun testProcessDirUnwritable() { + val unwritableDir = File(tempDir, "unwritable") + unwritableDir.mkdir() + File(unwritableDir, "test.txt").createNewFile() + + try { + Assume.assumeTrue(unwritableDir.setWritable(false, false)) + // This should not throw an exception + process_dir(unwritableDir) + + // The index.html should not have been created + assertFalse(File(unwritableDir, "index.html").exists()) + } finally { + unwritableDir.setWritable(true, false) + } + } }