From ca41486c4598b2592dbaba66c5016237b1e4c15f Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 10 May 2026 23:53:17 -0400 Subject: [PATCH] security+a11y: job ownership check, aria-live chips, aria-hidden topo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit security: - Fix bare open(sentinel, 'w').close() file descriptor leak; use context manager instead - Store requesting username in _diag_jobs at creation time; return 403 from api_diagnose_poll if the polling user does not match the job owner accessibility: - Add aria-live="polite" aria-atomic="true" to .status-chips container so screen readers announce critical/warning count changes on refresh - Add aria-controls="events-table-wrap" to critical and warning stat cards so assistive tech knows these buttons control the events table - Add aria-hidden sync to topology setCollapsed() — hidden topology content is now removed from the accessibility tree when collapsed, preventing keyboard focus from entering invisible elements Co-Authored-By: Claude Sonnet 4.6 --- app.py | 10 ++++++++-- templates/index.html | 9 ++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 9b5833a..a8a9d72 100644 --- a/app.py +++ b/app.py @@ -436,8 +436,12 @@ def api_diagnose_start(): return jsonify({'error': 'Resolved interface name contains invalid characters'}), 400 job_id = str(uuid.uuid4()) + requesting_user = _get_user()['username'] with _diag_lock: - _diag_jobs[job_id] = {'status': 'running', 'result': None, 'created_at': time.time()} + _diag_jobs[job_id] = { + 'status': 'running', 'result': None, + 'created_at': time.time(), 'user': requesting_user, + } def _run(): try: @@ -467,6 +471,8 @@ def api_diagnose_poll(job_id: str): job = _diag_jobs.get(job_id) if not job: return jsonify({'error': 'Job not found'}), 404 + if job.get('user') != _get_user()['username']: + return jsonify({'error': 'Forbidden'}), 403 return jsonify({'status': job['status'], 'result': job.get('result')}) @@ -524,7 +530,7 @@ def api_avatar(): return '', 404 if not avatar_data or len(avatar_data) < 100: - open(sentinel, 'w').close() + with open(sentinel, 'w'): pass return '', 404 # Validate JPEG magic bytes (FF D8 FF) diff --git a/templates/index.html b/templates/index.html index bc5af00..cdc7f6f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,7 +5,7 @@
-
+
{% if not daemon_ok %} ⚠ MONITOR OFFLINE {% endif %} @@ -30,7 +30,8 @@
+ data-stat-filter="critical" aria-label="{{ summary.critical or 0 }} critical alerts" + aria-controls="events-table-wrap">
{{ summary.critical or 0 }} @@ -39,7 +40,8 @@
+ data-stat-filter="warning" aria-label="{{ summary.warning or 0 }} warning alerts" + aria-controls="events-table-wrap">
{{ summary.warning or 0 }} @@ -484,6 +486,7 @@ function setCollapsed(v) { wrap.classList.toggle('is-collapsed', v); + wrap.setAttribute('aria-hidden', v ? 'true' : 'false'); btn.setAttribute('aria-expanded', v ? 'false' : 'true'); btn.textContent = v ? '▾ Expand' : '▴ Collapse'; try { localStorage.setItem(LS_KEY, v ? '1' : '0'); } catch(_) {}