diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a885865d..d00e08e1 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -99,3 +99,7 @@ **Root cause:** The protected implementation added canonical names to the exclusion set but did not compare each observed directory entry through a locale-stable normalized key. **Prevention:** Build one `Locale.ROOT` lowercase set from the canonical sensitive names, compare every observed name against it, and add the original spelling to the exclusion set so downstream exact membership remains correct. **Evidence:** `testProcessIgnoreFileTreatsSensitiveNamesCaseInsensitively` failed on test-only commit `472b916cd40f70693c4e1eb48956042a25353feb` (CI run `31469596932`) and passed with the source fix at `bb113d858ccfc42ddaecf6729749b238e5ade2d0` (CI run `31469921661`). +## 2024-05-24 - BiDi 스푸핑 취약점 (CVE-2021-42574) +**Vulnerability:** 파일명에 유니코드 양방향(BiDi) 제어 문자(예: RLO, LRI)가 포함될 경우 UI에서 확장자가 스푸핑되어 악성 파일(예: `.exe`가 `.txt`로 보임)이 다운로드될 수 있는 Trojan Source 취약점이 발견되었습니다. +**Learning:** `escapeHtml()` 함수에서 HTML 예약어 이외의 렌더링에 영향을 미치는 숨겨진 제어 문자는 기본적으로 필터링되지 않았으며, `dir="auto"`와 같은 단순한 설정은 `` 등의 속성에서 완벽히 격리되지 않습니다. +**Prevention:** 렌더링 시 악용될 수 있는 `\u202E`, `\u2066` 등의 BiDi 제어 문자를 `escapeHtml()`에서 명시적으로 리터럴(예: `\\u202E`)로 이스케이프하여 노출시킵니다. 또한, 디렉토리명 및 파일명 등 사용자가 통제 가능한 임의의 문자열을 HTML에 렌더링할 때는 W3C 권고안에 따라 First Strong Isolate (`⁨`) 및 Pop Directional Isolate (`⁩`)를 래핑하여 텍스트의 방향성을 구조적으로 격리해야 합니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index c442b3b1..563acc08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- BiDi(양방향) 텍스트 제어 문자를 이스케이프하고 FSI/PDI 래퍼를 적용하여 파일명/확장자 스푸핑(Trojan Source) 취약점 수정 # Changelog All notable changes to this project are documented in this file. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 0972fa2c..48a39271 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -56,7 +56,7 @@ a:hover, a:focus-visible { outline: 2px solid #0969da; outline-offset: -2px; } -a:hover span:last-child, a:focus-visible span:last-child { +a:hover .entry-name, a:focus-visible .entry-name { text-decoration: underline; } @media (prefers-reduced-motion: reduce) { @@ -243,6 +243,15 @@ fun String.escapeHtml(): String { '"' -> """ '\'' -> "'" '`' -> "`" + '\u202A' -> "\\u202A" + '\u202B' -> "\\u202B" + '\u202C' -> "\\u202C" + '\u202D' -> "\\u202D" + '\u202E' -> "\\u202E" + '\u2066' -> "\\u2066" + '\u2067' -> "\\u2067" + '\u2068' -> "\\u2068" + '\u2069' -> "\\u2069" else -> null } if (replacement != null) { @@ -421,12 +430,12 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array <!-- 보안 향상: 리퍼러를 통한 디렉토리 경로 노출 방지 --> <meta name="referrer" content="no-referrer"> <meta name="robots" content="noindex, nofollow"> - <title>${directoryName.escapeHtml()} - 디렉토리 목록 + ⁨${directoryName.escapeHtml()}⁩ - 디렉토리 목록
-

${directoryName.escapeHtml()}

+

⁨${directoryName.escapeHtml()}⁩