Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)하도록 구성해야 합니다.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [Unreleased]
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
### Security
- i18n 스크립트에 브라우저 API 환경 검증 추가로 SSR 호환성 및 안전한 실패(Fail securely) 기능 강화
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated

# CHANGELOG

## [Unreleased]
Expand Down
31 changes: 22 additions & 9 deletions i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -385,16 +394,20 @@ 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
}
currentLang = 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());
9 changes: 9 additions & 0 deletions tests/test_i18n_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
seonghobae marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Loading