From 956cb826d771e897184d288781e315171f249448 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:59:36 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 5 +++++ i18n.js | 35 +++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index fa902ea..33d744a 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -47,3 +47,8 @@ **Vulnerability:** 사용자 입력값(`lang`)을 검증 없이 `console.warn`과 같은 로그 함수에 그대로 보간하여 출력할 경우, 로그 인젝션(Log Forging) 공격에 노출될 수 있음. **Learning:** 사용자 입력이 포함된 문자열을 직접 보간하면 악의적인 페이로드가 로그 파일에 주입되어 로그 분석 시스템을 방해하거나 다른 취약점을 연계할 수 있음. **Prevention:** 로그를 남길 때는 검증되지 않은 외부 입력값을 동적으로 문자열에 주입(Interpolation)하는 대신, 사전에 정의된 정적이고 안전한 메시지로 대체해야 함. + +## 2026-08-29 - 브라우저 API 환경 검증을 통한 가용성 확보 +**Vulnerability:** 공유 유틸리티 스크립트(`i18n.js`)에서 환경 검증(예: `typeof window !== 'undefined'`) 없이 브라우저 전용 API(`window`, `localStorage`, `document`, `navigator`)에 접근할 경우, SSR(Server-Side Rendering) 환경이나 비브라우저 환경에서 실행 시 처리되지 않은 예외(Unhandled Exception)가 발생하여 스크립트 실행이 중단되는 가용성 문제가 있었습니다. +**Learning:** 정적 사이트라 하더라도 유틸리티 스크립트가 다양한 렌더링 컨텍스트(예: 빌드 단계, 테스트 환경, 추후 SSR 도입 시 등)에서 호출될 수 있으므로, 방어적 프로그래밍 관점에서 외부 API 호출 전에는 반드시 환경 컨텍스트를 검증해야 함을 확인했습니다. +**Prevention:** 브라우저 전역 객체에 접근하기 전에 항상 `typeof window !== 'undefined'` 와 같은 환경 검증 검사를 추가하여(fail securely 원칙 준수) 예측 불가능한 환경에서도 애플리케이션의 가용성을 보호해야 합니다. diff --git a/i18n.js b/i18n.js index 00b81d3..abf493c 100644 --- a/i18n.js +++ b/i18n.js @@ -297,17 +297,26 @@ 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 "ko"; } // ⚡ Bolt: Cache DOM queries and current state to prevent redundant lookups and layout thrashing @@ -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,12 @@ 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()); +if (typeof window !== 'undefined') { + setLanguage(preferredLanguage()); +} From 8a052a37c0adc2507ca231d3a2c8147e403dc006 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:06:05 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From ebbcf8de5ab417e6f981e1a32e36275fac4c74d4 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:10:07 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 2d83c5cd9010d5f8ecd88280d7a2eb43b6ad64ad Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:16:08 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_i18n_security.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_i18n_security.py b/tests/test_i18n_security.py index 833b464..2007b5f 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 browser APIs are guarded by environment checks.""" + 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 f4818b2d2ade10b921b588e84603b1700b986aa6 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:28:25 +0000 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 191decd417c7e820ffc00f804be35ea583fb88ba Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:39:24 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a1911652b098527c95f1c5b5911b5f110fddf770 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:52:18 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit