Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG

## [Unreleased]
- **외부 링크 회귀 방지**: 현재 `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 효과는 배포 후 실측 대상으로 유지합니다.
Expand Down
33 changes: 33 additions & 0 deletions tests/test_index_security.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
"""Security regression tests for the main page (index.html)."""

import re
from html.parser import HTMLParser
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
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")
Expand Down Expand Up @@ -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()
Expand All @@ -60,3 +77,19 @@ def test_index_has_no_inline_active_content() -> None:
assert (
'<meta name="referrer" content="strict-origin-when-cross-origin">' in html
)


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}"
)
Loading