From 2fb1a49ebc42b2d79edf60954b3955c6858eb9fb Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:53:29 +0000 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[secu?= =?UTF-8?q?rity=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 4 ++++ CHANGELOG.md | 4 ++++ i18n.js | 31 ++++++++++++++++++++++--------- tests/test_i18n_security.py | 9 +++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index fa902ea..ec1058a 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -47,3 +47,7 @@ **Vulnerability:** 사용자 입력값(`lang`)을 검증 없이 `console.warn`과 같은 로그 함수에 그대로 보간하여 출력할 경우, 로그 인젝션(Log Forging) 공격에 노출될 수 있음. **Learning:** 사용자 입력이 포함된 문자열을 직접 보간하면 악의적인 페이로드가 로그 파일에 주입되어 로그 분석 시스템을 방해하거나 다른 취약점을 연계할 수 있음. **Prevention:** 로그를 남길 때는 검증되지 않은 외부 입력값을 동적으로 문자열에 주입(Interpolation)하는 대신, 사전에 정의된 정적이고 안전한 메시지로 대체해야 함. +## 2026-08-30 - 브라우저 API 접근 전 환경 검증 로직 추가 +**Vulnerability:** 브라우저 API(`window`, `document`, `localStorage`)에 접근할 때 환경(SSR 등) 검증 없이 호출하여 발생할 수 있는 가용성 저하 및 에러 노출 위험. +**Learning:** 공용 유틸리티 스크립트에서 환경 검증 없이 브라우저 전용 API를 호출하면 SSR(Server-Side Rendering) 환경이나 제한된 브라우저 환경에서 스크립트 크래시가 발생할 수 있습니다. +**Prevention:** 브라우저 API에 접근하기 전에 항상 `typeof window !== 'undefined'` 와 같은 환경 검증을 수행하여 견고성을 높이고 안전하게 실패(Fail securely)하도록 구성해야 합니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index e097a55..a6766b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] +### Security +- i18n 스크립트에 브라우저 API 환경 검증 추가로 SSR 호환성 및 안전한 실패(Fail securely) 기능 강화 + # CHANGELOG ## [Unreleased] diff --git a/i18n.js b/i18n.js index 00b81d3..f67c479 100644 --- a/i18n.js +++ b/i18n.js @@ -297,17 +297,25 @@ const messages = { function preferredLanguage() { const allowed = ["ko", "en"]; - const query = new URLSearchParams(window.location.search).get("lang"); - if (allowed.includes(query)) return query; + + if (typeof window !== 'undefined' && window.location) { + const query = new URLSearchParams(window.location.search).get("lang"); + if (allowed.includes(query)) return query; + } try { - const saved = localStorage.getItem("cwl-language"); - if (allowed.includes(saved)) return saved; + if (typeof window !== 'undefined' && window.localStorage) { + const saved = window.localStorage.getItem("cwl-language"); + if (allowed.includes(saved)) return saved; + } } catch (error) { // Fail securely: ignore localStorage errors in strict privacy modes } - return navigator.language?.toLowerCase().startsWith("ko") ? "ko" : "en"; + if (typeof navigator !== 'undefined' && navigator.language) { + return navigator.language.toLowerCase().startsWith("ko") ? "ko" : "en"; + } + return "en"; } // ⚡ Bolt: Cache DOM queries and current state to prevent redundant lookups and layout thrashing @@ -327,6 +335,7 @@ function setLanguage(lang) { } if (currentLang === lang) return; // Skip if already in the requested language + if (typeof document === 'undefined') return; const dict = messages[lang] || messages.ko; @@ -385,7 +394,9 @@ function setLanguage(lang) { }); try { - localStorage.setItem("cwl-language", lang); + if (typeof window !== 'undefined' && window.localStorage) { + window.localStorage.setItem("cwl-language", lang); + } } catch (error) { // Fail securely: ignore localStorage errors } @@ -393,8 +404,10 @@ function setLanguage(lang) { } // Event listeners can just use the initial querySelectorAll -document.querySelectorAll("[data-lang]").forEach((button) => { - button.addEventListener("click", () => setLanguage(button.dataset.lang)); -}); +if (typeof document !== 'undefined') { + document.querySelectorAll("[data-lang]").forEach((button) => { + button.addEventListener("click", () => setLanguage(button.dataset.lang)); + }); +} setLanguage(preferredLanguage()); diff --git a/tests/test_i18n_security.py b/tests/test_i18n_security.py index 833b464..93fcab7 100644 --- a/tests/test_i18n_security.py +++ b/tests/test_i18n_security.py @@ -23,3 +23,12 @@ def test_i18n_avoids_log_injection() -> None: content = f.read() assert 'console.warn("[Security] Invalid language requested. Falling back to default.");' in content + +def test_i18n_environment_validation() -> None: + """Test that window and document are validated for SSR compatibility and availability.""" + with open("i18n.js", "r", encoding="utf-8") as f: + content = f.read() + + assert "typeof window !== 'undefined'" in content + assert "typeof document !== 'undefined'" in content + assert "typeof navigator !== 'undefined'" in content From ec2578f774df8a8bbe374565e526310e23fa4e35 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:01:32 +0000 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[secu?= =?UTF-8?q?rity=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6766b0..57e9313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,7 @@ -## [Unreleased] -### Security -- i18n 스크립트에 브라우저 API 환경 검증 추가로 SSR 호환성 및 안전한 실패(Fail securely) 기능 강화 - # CHANGELOG ## [Unreleased] +- **보안 개선**: i18n 스크립트에 브라우저 API 환경 검증 추가로 SSR 호환성 및 안전한 실패(Fail securely) 기능 강화 - **보안 개선**: `i18n.js`에서 잘못된 언어 요청 시 `console.warn` 메시지에 사용자 입력값이 직접 포함되지 않도록 수정하여 로그 인젝션(Log Injection) 취약점을 제거했습니다. - **성능 개선**: `.skip-link` 애니메이션을 `top`에서 `transform: translateY()`로 변경하여 전환 중 레이아웃 재계산을 줄일 수 있도록 했습니다. 실제 효과는 브라우저별 측정 대상입니다. - **렌더링 힌트 정합성**: 첫 화면의 eager 이미지와 단일 LCP 후보에서 강제 `decoding="async"`를 제거해 HTML 표준의 기본 `auto` 판단에 맡기고, 지연 로드 이미지에는 비동기 디코딩 힌트를 유지했습니다. 정적 테스트가 eager, lazy, LCP 후보 집합의 존재와 조합을 검증하며, 실제 LCP 효과는 배포 후 실측 대상으로 유지합니다. From 5d718ef44e22835b9129c1570cd7dace465a9979 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:06:45 +0000 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[secu?= =?UTF-8?q?rity=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 07abd44b7e65ae6f26c66676d4177563574bce93 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:24:34 +0000 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[secu?= =?UTF-8?q?rity=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 5f21260f1645869317db6f0a84030c4646d10216 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:11:49 +0000 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[secu?= =?UTF-8?q?rity=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 00f72547421adc0f9196bbe970bd53e96eecd245 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:05:10 +0900 Subject: [PATCH 06/13] test(i18n): execute non-browser runtime contract --- tests/test_i18n_security.py | 39 +++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/tests/test_i18n_security.py b/tests/test_i18n_security.py index 93fcab7..51407b0 100644 --- a/tests/test_i18n_security.py +++ b/tests/test_i18n_security.py @@ -1,4 +1,10 @@ -"""Test i18n security input validation.""" +"""Test i18n security and runtime boundary behavior.""" + +import shutil +import subprocess + +import pytest + def test_i18n_input_validation() -> None: """Test that allowedLanguages validation logic is correctly implemented in i18n.js.""" @@ -9,6 +15,7 @@ def test_i18n_input_validation() -> None: assert "allowedLanguages = [\"ko\", \"en\"]" in content or "allowedLanguages = ['ko', 'en']" in content assert "allowedLanguages.includes" in content + def test_i18n_html_security_tests_present() -> None: """Test that explicit __proto__ and XSS payload checks exist in the HTML test harness.""" with open("test_i18n.html", "r", encoding="utf-8") as f: @@ -17,6 +24,7 @@ def test_i18n_html_security_tests_present() -> None: assert "setLanguage(\"__proto__\")" in content assert "setLanguage(\"