From 231b2c123be8da81a22d3c3ad846306762323eca Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Fri, 4 Sep 2026 14:09:00 +0000
Subject: [PATCH 1/3] =?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
---
CHANGELOG.md | 1 +
tests/test_index_security.py | 13 +++++++++++++
2 files changed, 14 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e097a55..33de5b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# CHANGELOG
## [Unreleased]
+- **보안 개선**: 향후 `target="_blank"` 링크 추가 시 `rel="noopener noreferrer"` 속성 누락으로 인한 Reverse Tabnabbing 취약점이 발생하는 것을 방지하기 위해 정적 분석 테스트를 추가했습니다.
- **보안 개선**: `i18n.js`에서 잘못된 언어 요청 시 `console.warn` 메시지에 사용자 입력값이 직접 포함되지 않도록 수정하여 로그 인젝션(Log Injection) 취약점을 제거했습니다.
- **성능 개선**: `.skip-link` 애니메이션을 `top`에서 `transform: translateY()`로 변경하여 전환 중 레이아웃 재계산을 줄일 수 있도록 했습니다. 실제 효과는 브라우저별 측정 대상입니다.
- **렌더링 힌트 정합성**: 첫 화면의 eager 이미지와 단일 LCP 후보에서 강제 `decoding="async"`를 제거해 HTML 표준의 기본 `auto` 판단에 맡기고, 지연 로드 이미지에는 비동기 디코딩 힌트를 유지했습니다. 정적 테스트가 eager, lazy, LCP 후보 집합의 존재와 조합을 검증하며, 실제 LCP 효과는 배포 후 실측 대상으로 유지합니다.
diff --git a/tests/test_index_security.py b/tests/test_index_security.py
index a558f32..240b984 100644
--- a/tests/test_index_security.py
+++ b/tests/test_index_security.py
@@ -60,3 +60,16 @@ def test_index_has_no_inline_active_content() -> None:
assert (
'' in html
)
+
+def test_index_external_links_have_noopener_noreferrer() -> None:
+ """Ensure all external links (target="_blank") have rel="noopener noreferrer" to prevent reverse tabnabbing."""
+ html = _index_html()
+
+ # Very basic HTML parsing for the specific tag to avoid brittle regex and avoid missing imports
+ links = re.findall(r']+>', html, flags=re.IGNORECASE)
+ for link in links:
+ if 'target="_blank"' in link or "target='_blank'" in link:
+ # Check for noopener and noreferrer anywhere in the tag
+ has_noopener = 'noopener' in link
+ has_noreferrer = 'noreferrer' in link
+ assert has_noopener and has_noreferrer, f"External link missing noopener or noreferrer: {link}"
\ No newline at end of file
From 9e889b1a168cf4bad5be0c74078ffc7d16c34381 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 4 Sep 2026 23:50:20 +0900
Subject: [PATCH 2/3] test(security): parse outbound link rel policy
semantically
---
tests/test_index_security.py | 42 ++++++++++++++++++++++++++----------
1 file changed, 31 insertions(+), 11 deletions(-)
diff --git a/tests/test_index_security.py b/tests/test_index_security.py
index 240b984..d722169 100644
--- a/tests/test_index_security.py
+++ b/tests/test_index_security.py
@@ -1,6 +1,7 @@
"""Security regression tests for the main page (index.html)."""
import re
+from html.parser import HTMLParser
from pathlib import Path
@@ -8,6 +9,21 @@
INDEX = ROOT / "index.html"
+class _BlankTargetAnchorParser(HTMLParser):
+ """Collect anchors that intentionally open a new browsing context."""
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.anchors: list[dict[str, str]] = []
+
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ if tag.lower() != "a":
+ return
+ normalized = {name.lower(): value or "" for name, value in attrs}
+ if normalized.get("target", "").lower() == "_blank":
+ self.anchors.append(normalized)
+
+
def _index_html() -> str:
"""Return the main index.html source."""
return INDEX.read_text(encoding="utf-8")
@@ -45,6 +61,7 @@ def test_index_declares_strict_csp() -> None:
assert "'unsafe-inline'" not in policy
assert "'unsafe-eval'" not in policy
+
def test_index_has_no_inline_active_content() -> None:
"""Strict CSP remains enforceable without inline script or style exceptions."""
html = _index_html()
@@ -61,15 +78,18 @@ def test_index_has_no_inline_active_content() -> None:
'' in html
)
-def test_index_external_links_have_noopener_noreferrer() -> None:
- """Ensure all external links (target="_blank") have rel="noopener noreferrer" to prevent reverse tabnabbing."""
- html = _index_html()
- # Very basic HTML parsing for the specific tag to avoid brittle regex and avoid missing imports
- links = re.findall(r']+>', html, flags=re.IGNORECASE)
- for link in links:
- if 'target="_blank"' in link or "target='_blank'" in link:
- # Check for noopener and noreferrer anywhere in the tag
- has_noopener = 'noopener' in link
- has_noreferrer = 'noreferrer' in link
- assert has_noopener and has_noreferrer, f"External link missing noopener or noreferrer: {link}"
\ No newline at end of file
+def test_blank_target_links_keep_explicit_opener_and_referrer_policy() -> None:
+ """New-context links keep explicit opener isolation and referrer suppression."""
+ parser = _BlankTargetAnchorParser()
+ parser.feed(_index_html())
+
+ assert parser.anchors, "index.html must exercise the outbound-link policy"
+ for anchor in parser.anchors:
+ rel_tokens = {token.lower() for token in anchor.get("rel", "").split()}
+ assert "noopener" in rel_tokens, (
+ f"target=_blank link must keep explicit opener isolation: {anchor}"
+ )
+ assert "noreferrer" in rel_tokens, (
+ f"target=_blank link must keep the product referrer policy: {anchor}"
+ )
From 7b921c0451e02c1563ae2644d3219a6d29cd3a43 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 4 Sep 2026 23:50:53 +0900
Subject: [PATCH 3/3] docs(security): separate opener and referrer contracts
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33de5b2..dc08985 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
# CHANGELOG
## [Unreleased]
-- **보안 개선**: 향후 `target="_blank"` 링크 추가 시 `rel="noopener noreferrer"` 속성 누락으로 인한 Reverse Tabnabbing 취약점이 발생하는 것을 방지하기 위해 정적 분석 테스트를 추가했습니다.
+- **외부 링크 회귀 방지**: 현재 `target="_blank"` 링크가 명시적 `rel="noopener noreferrer"` 정책을 유지하는지 DOM 파서 기반 테스트를 추가했습니다. `noopener`는 opener 격리, `noreferrer`는 referrer 비공개 의미를 각각 검증하며 둘을 하나의 Reverse Tabnabbing 요구사항으로 일반화하지 않습니다.
- **보안 개선**: `i18n.js`에서 잘못된 언어 요청 시 `console.warn` 메시지에 사용자 입력값이 직접 포함되지 않도록 수정하여 로그 인젝션(Log Injection) 취약점을 제거했습니다.
- **성능 개선**: `.skip-link` 애니메이션을 `top`에서 `transform: translateY()`로 변경하여 전환 중 레이아웃 재계산을 줄일 수 있도록 했습니다. 실제 효과는 브라우저별 측정 대상입니다.
- **렌더링 힌트 정합성**: 첫 화면의 eager 이미지와 단일 LCP 후보에서 강제 `decoding="async"`를 제거해 HTML 표준의 기본 `auto` 판단에 맡기고, 지연 로드 이미지에는 비동기 디코딩 힌트를 유지했습니다. 정적 테스트가 eager, lazy, LCP 후보 집합의 존재와 조합을 검증하며, 실제 LCP 효과는 배포 후 실측 대상으로 유지합니다.