Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

254 lines
8.5 KiB
Python

"""
Tests for the check-target-blank-rel pre-commit hook.
The hook flags any ``<a target="_blank">`` that is not provably same-origin
and is missing ``rel="noopener noreferrer"`` — without ``noopener`` the opened
page can reach back via ``window.opener`` (tabnabbing), and without
``noreferrer`` the Referer header leaks the LDR URL to the destination.
The hook uses a "flag unless proven internal" policy: a new-tab link must
carry the rel unless we can statically prove it stays same-origin. That means
dynamic hrefs (JS ``${...}`` and Jinja ``{{ ... }}`` that are not
``url_for()``) and bare scheme-less hosts ARE flagged — those are exactly the
shapes where past regressions lived. Provably same-origin hrefs (leading
``/``, ``#``, ``?``, a Jinja ``url_for``, or a ``mailto:``/``tel:`` scheme)
are skipped.
"""
import subprocess
import sys
import tempfile
from pathlib import Path
import pytest
HOOK_SCRIPT = (
Path(__file__).parent.parent.parent
/ ".pre-commit-hooks"
/ "check-target-blank-rel.py"
)
def _run_hook(
content: str, filename: str = "x.html"
) -> subprocess.CompletedProcess:
"""Write content to a temp file and run the hook against it."""
with tempfile.TemporaryDirectory() as d:
path = Path(d) / filename
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
return subprocess.run(
[sys.executable, str(HOOK_SCRIPT), str(path)],
capture_output=True,
text=True,
)
class TestFlagsStaticExternalMissingRel:
"""Static external new-tab anchors missing either rel token are flagged."""
def test_external_no_rel_is_flagged(self):
result = _run_hook(
'<a href="https://example.com" target="_blank">x</a>\n'
)
assert result.returncode == 1
assert "noopener noreferrer" in result.stdout
def test_external_noopener_only_is_flagged(self):
"""`rel="noopener"` alone still leaks the Referer — must fail."""
result = _run_hook(
'<a href="https://example.com" target="_blank" '
'rel="noopener">x</a>\n'
)
assert result.returncode == 1
def test_external_noreferrer_only_is_flagged(self):
result = _run_hook(
'<a href="https://example.com" target="_blank" '
'rel="noreferrer">x</a>\n'
)
assert result.returncode == 1
def test_http_external_is_flagged(self):
result = _run_hook(
'<a href="http://example.com" target="_blank">x</a>\n'
)
assert result.returncode == 1
def test_protocol_relative_external_is_flagged(self):
result = _run_hook('<a href="//example.com" target="_blank">x</a>\n')
assert result.returncode == 1
class TestFlagsDynamicMissingRel:
"""The point of the flip: hrefs not provably internal are flagged too."""
def test_js_template_literal_href_is_flagged(self):
"""`${...}` is not provably same-origin — it must carry rel."""
result = _run_hook(
'<a href="${escapeAttr(resource.url)}" target="_blank">x</a>\n',
"x.js",
)
assert result.returncode == 1
def test_jinja_expression_href_is_flagged(self):
"""A non-url_for `{{ ... }}` (e.g. an external source URL) needs rel."""
result = _run_hook(
'<a href="{{ document.original_url }}" target="_blank">x</a>\n'
)
assert result.returncode == 1
def test_jinja_statement_wrapped_href_is_flagged(self):
"""`{% if %}...{% endif %}` could resolve external — not provable."""
result = _run_hook(
'<a href="{% if x %}https://a{% else %}/b{% endif %}" '
'target="_blank">x</a>\n'
)
assert result.returncode == 1
def test_bare_scheme_less_host_is_flagged(self):
result = _run_hook('<a href="example.com" target="_blank">x</a>\n')
assert result.returncode == 1
def test_literal_scheme_prefix_before_template_expr_is_flagged(self):
result = _run_hook(
'<a href="https://${host}/p" target="_blank">x</a>\n', "x.js"
)
assert result.returncode == 1
class TestCompliantAnchorsPass:
"""Correctly-protected anchors pass regardless of static/dynamic href."""
def test_both_tokens_present_passes(self):
result = _run_hook(
'<a href="https://example.com" target="_blank" '
'rel="noopener noreferrer">x</a>\n'
)
assert result.returncode == 0
def test_reversed_token_order_passes(self):
result = _run_hook(
'<a href="https://example.com" target="_blank" '
'rel="noreferrer noopener">x</a>\n'
)
assert result.returncode == 0
def test_uppercase_rel_tokens_pass(self):
result = _run_hook(
'<a href="https://example.com" target="_blank" '
'rel="NOOPENER NOREFERRER">x</a>\n'
)
assert result.returncode == 0
def test_extra_rel_tokens_pass(self):
result = _run_hook(
'<a href="https://example.com" target="_blank" '
'rel="nofollow noopener noreferrer">x</a>\n'
)
assert result.returncode == 0
def test_dynamic_href_with_rel_passes(self):
"""A `${...}` href is fine once it carries the rel."""
result = _run_hook(
'<a href="${u}" target="_blank" rel="noopener noreferrer">x</a>\n',
"x.js",
)
assert result.returncode == 0
class TestProvablyInternalSkipped:
"""Provably same-origin hrefs are skipped even without rel."""
@pytest.mark.parametrize(
"href",
[
"/library/document/1/pdf",
"/progress/${encodeURIComponent(id)}", # dynamic but same-origin
"#section",
"?tab=settings",
"{{ url_for('main.index') }}",
"{{- url_for('main.index') }}",
"mailto:hi@example.com",
"tel:+123",
"javascript:void(0)",
],
)
def test_internal_href_with_blank_target_passes(self, href: str):
result = _run_hook(f'<a href="{href}" target="_blank">x</a>\n')
assert result.returncode == 0, result.stdout
def test_anchor_without_blank_target_is_ignored(self):
result = _run_hook('<a href="https://example.com">x</a>\n')
assert result.returncode == 0
class TestSchemeCaseInsensitive:
"""Uppercase URL schemes must not evade the check."""
@pytest.mark.parametrize(
"href",
["HTTPS://example.com", "HTTP://example.com", "HtTpS://example.com"],
)
def test_uppercase_scheme_external_is_flagged(self, href: str):
result = _run_hook(f'<a href="{href}" target="_blank">x</a>\n')
assert result.returncode == 1, result.stdout
class TestParsingRobustness:
"""Attribute order, quoting, casing, and multi-line tags all parse."""
def test_target_before_href_is_flagged(self):
result = _run_hook(
'<a target="_blank" href="https://example.com">x</a>\n'
)
assert result.returncode == 1
def test_single_quoted_attrs_are_flagged(self):
result = _run_hook(
"<a href='https://example.com' target='_blank'>x</a>\n"
)
assert result.returncode == 1
def test_mixed_case_blank_target_is_flagged(self):
result = _run_hook(
'<a href="https://example.com" TARGET="_BLANK">x</a>\n'
)
assert result.returncode == 1
def test_multiline_tag_missing_rel_is_flagged(self):
"""Multi-line attribute lists must still be parsed (no DOTALL needed)."""
result = _run_hook(
'<a href="https://example.com"\n'
' target="_blank"\n'
' class="x">link</a>\n'
)
assert result.returncode == 1
def test_multiline_tag_with_rel_passes(self):
result = _run_hook(
'<a href="https://example.com"\n'
' target="_blank"\n'
' rel="noopener noreferrer">link</a>\n'
)
assert result.returncode == 0
class TestExitCodes:
"""No anchors / no violations → clean exit."""
def test_no_anchors_passes(self):
result = _run_hook("<div>no anchors here</div>\n")
assert result.returncode == 0
def test_multiple_violations_reported(self):
content = (
'<a href="https://a.com" target="_blank">a</a>\n'
'<a href="https://b.com" target="_blank" rel="noopener">b</a>\n'
)
result = _run_hook(content)
assert result.returncode == 1
assert "line 1" in result.stdout
assert "line 2" in result.stdout