security: enforce strict browser execution boundary (#295)
CI / lint (pull_request) Successful in 29s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-08 11:38:18 +00:00
1 parent 54029d2668
commit 6e6e63e553
28 files changed
+3435 -3287

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
from pathlib import Path
from src.views import dashboard as dashboard_html
FRONTEND = Path(__file__).resolve().parents[1] / "frontend"
def dashboard_bundle_text() -> str:
"""Return the HTML, CSS, and bootstrap sources that make up the dashboard."""
return "\n".join(
(
(FRONTEND / "index.html").read_text(),
(FRONTEND / "dashboard.css").read_text(),
(FRONTEND / "dashboard.js").read_text(),
)
)
async def dashboard() -> str:
# Exercise the real view lookup before adding its separately served assets.
html = await dashboard_html()
return "\n".join(
(
html,
(FRONTEND / "dashboard.css").read_text(),
(FRONTEND / "dashboard.js").read_text(),
)
)
+2 -3
View File
@@ -2,12 +2,11 @@ import re
from pathlib import Path
from urllib.parse import urljoin
DASHBOARD_HTML = Path(__file__).parent.parent / "frontend" / "index.html"
from tests.dashboard_bundle import dashboard_bundle_text
def test_api_requests_resolve_inside_dashboard_subpath():
html = DASHBOARD_HTML.read_text()
html = dashboard_bundle_text()
api_paths = re.findall(r"fetch\(['\"]([^'\"]*api/v1/[^'\"]*)['\"]", html)
assert api_paths
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
OUTBOX = Path(__file__).parents[1] / "frontend" / "authored-outbox.js"
+1 -1
View File
@@ -6,7 +6,7 @@ import httpx
import pytest
from src import main
from src.views import dashboard
from tests.dashboard_bundle import dashboard
SYNC = Path(__file__).parents[1] / "frontend" / "background-issue-sync.js"
+6 -4
View File
@@ -4,6 +4,8 @@ from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urljoin
from tests.dashboard_bundle import dashboard_bundle_text
FRONTEND = Path(__file__).parents[1] / "frontend"
COMMANDS = FRONTEND / "commands.js"
@@ -41,7 +43,7 @@ if (action !== 'refresh') throw new Error(`expected refresh, got ${{action}}`);
def test_command_script_resolves_inside_dashboard_subpath():
parser = ScriptSourceParser()
parser.feed((FRONTEND / "index.html").read_text())
parser.feed(dashboard_bundle_text())
command_source = next(source for source in parser.sources if source.endswith("commands.js"))
assert urljoin(
@@ -152,7 +154,7 @@ if (commands.nextSelection(1, 'Enter', 3) !== 1) throw new Error('other keys sho
def test_palette_exposes_accessible_global_work_search_under_dashboard_subpath():
html = (FRONTEND / "index.html").read_text()
html = dashboard_bundle_text()
assert 'role="combobox"' in html
assert 'aria-controls="cmd-results"' in html
@@ -221,7 +223,7 @@ if (!states.some(state => state.status === 'claimed')) throw new Error('claim co
def test_remote_search_selection_opens_native_preview_without_navigation():
html = (FRONTEND / "index.html").read_text()
html = dashboard_bundle_text()
run_item = html.split("function runCommandItem(item)", 1)[1].split(
"function renderCommands", 1
@@ -233,7 +235,7 @@ def test_remote_search_selection_opens_native_preview_without_navigation():
def test_assigned_issue_preview_hands_off_to_existing_my_work_sheet():
html = (FRONTEND / "index.html").read_text()
html = dashboard_bundle_text()
assert "detail.claimable || (detail.assigned_to_me && detail.kind === 'issue')" in html
assert "claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me'" in html
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
ROOT = Path(__file__).resolve().parents[1]
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
DRAFTS = Path(__file__).parents[1] / "frontend" / "drafts.js"
+4 -3
View File
@@ -6,13 +6,14 @@ import pytest
from src import main
from src import gitea_proxy
from tests.dashboard_bundle import dashboard_bundle_text
DASHBOARD = Path("frontend/index.html")
def test_dashboard_has_realtime_gitea_event_stream_widget():
html = DASHBOARD.read_text()
html = dashboard_bundle_text()
assert "Gitea event stream" in html
assert "id=\"gitea-events\"" in html
@@ -25,14 +26,14 @@ def test_dashboard_has_realtime_gitea_event_stream_widget():
def test_event_stream_reports_when_activity_was_refreshed():
html = DASHBOARD.read_text()
html = dashboard_bundle_text()
assert 'id="gitea-events-status"' in html
assert "setEventStreamStatus('Updated ' + fmt(new Date()))" in html
def test_event_stream_reports_refresh_failure_without_erasing_visible_events():
html = DASHBOARD.read_text()
html = dashboard_bundle_text()
assert "setEventStreamStatus('Update failed · showing last activity')" in html
assert "qs('#gitea-events').innerHTML = '<div class=\"muted\">Event stream unavailable.</div>'" not in html
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
INSTALL_APP = Path(__file__).resolve().parents[1] / "frontend" / "install-app.js"
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
OUTBOX = Path(__file__).parents[1] / "frontend" / "issue-outbox.js"
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
OFFLINE_WORK = Path(__file__).parents[1] / "frontend" / "offline-work.js"
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
ROOT = Path(__file__).parents[1]
+4 -2
View File
@@ -1,11 +1,13 @@
from pathlib import Path
from tests.dashboard_bundle import dashboard_bundle_text
DASHBOARD = Path("frontend/index.html")
def test_dashboard_panels_are_independently_collapsible():
html = DASHBOARD.read_text()
html = dashboard_bundle_text()
for panel_key in ("context", "ai-stream", "issues", "pull-requests", "widgets", "layout", "markdown"):
assert f'<details class="panel' in html
@@ -13,7 +15,7 @@ def test_dashboard_panels_are_independently_collapsible():
def test_panel_open_state_is_restored_and_saved_in_local_storage():
html = DASHBOARD.read_text()
html = dashboard_bundle_text()
assert 'const PANEL_STATE_KEY = "stackchain.panel-state.v1"' in html
assert "localStorage.getItem(PANEL_STATE_KEY)" in html
+59
View File
@@ -0,0 +1,59 @@
import httpx
import pytest
from src import main
REQUIRED_HEADERS = {
"x-content-type-options": "nosniff",
"referrer-policy": "no-referrer",
"x-frame-options": "DENY",
}
def assert_browser_security_boundary(response: httpx.Response) -> None:
for name, value in REQUIRED_HEADERS.items():
assert response.headers[name] == value
assert "camera=()" in response.headers["permissions-policy"]
policy = response.headers["content-security-policy"]
assert "default-src 'self'" in policy
assert "script-src 'self'" in policy
assert "object-src 'none'" in policy
assert "frame-ancestors 'none'" in policy
script_policy = next(part for part in policy.split(";") if "script-src" in part)
assert "'unsafe-inline'" not in script_policy
assert "'unsafe-eval'" not in script_policy
@pytest.mark.anyio
async def test_security_boundary_covers_pages_health_static_and_api_errors(monkeypatch):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", "access-token-with-at-least-thirty-two-characters")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", "session-secret-with-at-least-thirty-two-characters")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
responses = [
await client.get("/healthz"),
await client.get("/login"),
await client.get("/static/dashboard.js"),
await client.get("/api/v1/context"),
]
assert [response.status_code for response in responses] == [200, 200, 200, 401]
for response in responses:
assert_browser_security_boundary(response)
@pytest.mark.anyio
async def test_security_boundary_covers_fail_closed_authentication_response(monkeypatch):
monkeypatch.delenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", raising=False)
monkeypatch.delenv("STACKCHAIN_DASHBOARD_AUTH_MODE", raising=False)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
response = await client.get("/login")
assert response.status_code == 503
assert_browser_security_boundary(response)
+6 -2
View File
@@ -92,10 +92,12 @@ async function dispatchNotificationClick(route) {{
return json.loads(completed.stdout)
def test_mobile_install_flow_ships_in_a_new_shell_cache():
def test_strict_browser_assets_ship_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v25" in source
assert "stackchain-dashboard-shell-v26" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -196,6 +198,8 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
assert set(result["added"]) == {
"/dashboard/",
"/dashboard/manifest.webmanifest",
"/dashboard/static/dashboard.css",
"/dashboard/static/dashboard.js",
"/dashboard/static/icons/stackchain-192.png",
"/dashboard/static/icons/stackchain-512.png",
"/dashboard/static/session.js",
+1 -1
View File
@@ -1,6 +1,6 @@
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
@pytest.mark.anyio
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js"
+16 -3
View File
@@ -1,3 +1,5 @@
from pathlib import Path
import pytest
from src.views import dashboard
@@ -21,15 +23,26 @@ async def test_dashboard_viewport_allows_mobile_zoom():
assert "user-scalable=no" not in html
@pytest.mark.anyio
async def test_dashboard_bootstrap_uses_csp_compatible_static_assets():
html = await dashboard()
assert '<link rel="stylesheet" href="static/dashboard.css" />' in html
assert '<script src="static/dashboard.js"></script>' in html
assert "<style>" not in html
assert "<script>" not in html
@pytest.mark.anyio
async def test_global_search_preview_is_a_phone_safe_accessible_dialog():
html = await dashboard()
css = (Path(__file__).resolve().parents[1] / "frontend" / "dashboard.css").read_text()
assert 'id="search-preview" role="dialog" aria-modal="true"' in html
assert 'aria-labelledby="search-preview-title"' in html
assert 'id="claim-search-result"' in html
assert 'id="open-search-result-gitea"' in html
assert 'src="static/search-preview.js"' in html
assert ".search-preview-panel" in html
assert "height:100dvh" in html
assert "env(safe-area-inset-bottom)" in html
assert ".search-preview-panel" in css
assert "height:100dvh" in css
assert "env(safe-area-inset-bottom)" in css
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from src.views import dashboard
from tests.dashboard_bundle import dashboard
WIDGETS = Path(__file__).parents[1] / "frontend" / "widgets.js"
+6 -3
View File
@@ -1,3 +1,5 @@
from pathlib import Path
import httpx
import pytest
@@ -98,9 +100,10 @@ async def test_dashboard_announces_offline_mode_and_refreshes_after_reconnect():
assert 'id="offline-status"' in response.text
assert 'role="status"' in response.text
assert 'aria-live="polite"' in response.text
assert "window.addEventListener('offline'" in response.text
assert "window.addEventListener('online'" in response.text
assert "contextPoller.refresh()" in response.text
bootstrap = (Path(__file__).resolve().parents[1] / "frontend" / "dashboard.js").read_text()
assert "window.addEventListener('offline'" in bootstrap
assert "window.addEventListener('online'" in bootstrap
assert "contextPoller.refresh()" in bootstrap
@pytest.mark.anyio