diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html
index 132bc31b..0a75766e 100644
--- a/scanner/dashboard/index.html
+++ b/scanner/dashboard/index.html
@@ -96,6 +96,7 @@
+opens in a new tab
@@ -281,7 +282,8 @@ Dashboard
function openDetail(f){
lastFocus = document.activeElement;
const s = String(f.severity||'INFO').toUpperCase();
- const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
');
+ const extIcon = ``;
+ const refs = (f.references||[]).map(r=>`${esc(r)}${extIcon}`).join('
');
const owasp = (f.owasp||[]).join(', ');
const cwe = (f.cwe||[]).join(', ');
const d = document.getElementById('detail');
diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py
index 75a5809f..f6e9bac7 100644
--- a/tests/test_dashboard_core.py
+++ b/tests/test_dashboard_core.py
@@ -302,3 +302,48 @@ def test_dashboard_search_escape_clears_input():
assert "e.key === 'Escape'" in html
assert "query = '';" in html
assert "render();" in html
+
+def test_dashboard_external_links_have_accessible_visual_indicator():
+ """External links opening in new tabs must have both an assistive warning and a visual indicator (WCAG G201)."""
+ from html.parser import HTMLParser
+ import re
+
+ html = dashboard_index_path().read_text(encoding="utf-8")
+
+ # Verify the global description element exists
+ assert 'opens in a new tab' in html
+
+ refs_markup = re.search(
+ r"const refs\s*=\s*\(f\.references\|\|\[\]\)\.map\(r=>`(?P.*?)`\)\.join\('
'\);",
+ html,
+ flags=re.DOTALL,
+ )
+ assert refs_markup is not None
+
+ class _LinkAttributeParser(HTMLParser):
+ def __init__(self) -> None:
+ super().__init__()
+ self.links = []
+ self.svgs = []
+
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ if tag == "a":
+ self.links.append(dict(attrs))
+ if tag == "svg":
+ self.svgs.append(dict(attrs))
+
+ parser = _LinkAttributeParser()
+ # Inject a dummy href to make the template string parseable HTML
+ test_html = refs_markup.group("markup").replace('${esc(safeUrl(r))}', 'https://example.com').replace('${esc(r)}', 'Link').replace('${extIcon}', '')
+ parser.feed(test_html)
+
+ assert len(parser.links) == 1
+ link = parser.links[0]
+ assert link.get("target") == "_blank"
+ assert link.get("rel") == "noopener"
+ assert link.get("aria-describedby") == "ext-link-desc"
+
+ assert len(parser.svgs) >= 1
+ svg = parser.svgs[0]
+ assert svg.get("aria-hidden") == "true"
+ assert svg.get("focusable") == "false"