# Security Audit Report — STEAM Security Dashboard **Date:** 2026-04-01 **Scope:** Full codebase — backend routes, authentication, file handling, Python scripts, React frontend **Methodology:** Static analysis across four parallel audit tracks --- ## Executive Summary The audit identified **31 findings** across four severity levels. The most serious issues are concentrated in the **authentication and authorization layer** — several endpoints are either completely unauthenticated or have role-checking middleware called with the wrong arguments, silently bypassing access control. These require immediate remediation before the application is exposed to a broader user base. | Severity | Count | |----------|-------| | Critical | 6 | | High | 9 | | Medium | 10 | | Low / Info | 6 | | **Total** | **31** | The application has strong foundational security in several areas: all database queries use parameterized statements (no SQL injection risk), path traversal prevention is comprehensive, Python script execution uses `spawn` with argument arrays (no shell injection), and file type allowlisting is in place. The vulnerabilities are largely in middleware wiring and missing access controls rather than fundamental design flaws. --- ## Critical Findings --- ### C-1 — Missing Authentication on Ivanti Findings Endpoints **File:** `backend/routes/ivantiFindings.js:552–600` The findings router imports `requireRole` but **not** `requireAuth`. No authentication middleware is applied at the router level or on individual routes. Four endpoints are fully unauthenticated: ```js const { requireRole } = require('../middleware/auth'); // requireAuth never imported router.get('/', async (req, res) => { // line 552 — no auth router.post('/sync', async (req, res) => { // line 561 — no auth router.get('/counts', async (req, res) => { // line 571 — no auth router.get('/fp-workflow-counts', ...) // line 580 — no auth ``` **Impact:** Any unauthenticated attacker on the network can read the full list of Ivanti host findings (hostnames, IPs, CVEs, severity, SLA status), trigger a sync operation, and enumerate all finding metrics. **Fix:** Import `requireAuth` and apply it to the router or each route: ```js const { requireAuth, requireRole } = require('../middleware/auth'); router.use(requireAuth(db)); ``` --- ### C-2 — Broken requireRole Call — Privilege Escalation in Knowledge Base **File:** `backend/routes/knowledgeBase.js:43, 305` `requireRole` is called with `db` as the first argument: ```js router.post('/upload', requireAuth(db), requireRole(db, 'editor', 'admin'), ...) router.delete('/:id', requireAuth(db), requireRole(db, 'editor', 'admin'), ...) ``` The function signature is `function requireRole(...allowedRoles)`. It does not accept `db`. The database object is treated as the first "allowed role", so the check becomes `req.user.role === db` — an object comparison that always evaluates false, meaning **the check never blocks anyone**. Any authenticated viewer can upload and delete knowledge base documents. **Fix:** Remove `db` from all `requireRole` calls: ```js requireRole('editor', 'admin') ``` --- ### C-3 — Unauthenticated Ivanti Finding Note Writes **File:** `backend/routes/ivantiFindings.js:639` The PUT endpoint for saving finding notes has no authentication middleware: ```js router.put('/:findingId/note', (req, res) => { const note = String(req.body.note || '').slice(0, 255); db.run(`INSERT INTO ivanti_finding_notes ...`); }); ``` **Impact:** Any unauthenticated request can write notes to any finding. Notes are visible to all users and used during remediation triage. An attacker could inject false status information (e.g. "EXC-12345 — patched") to mislead the team or cover tracks. **Fix:** Add `requireAuth(db)` to this route. --- ### C-4 — No Brute Force Protection on Login Endpoint **File:** `backend/routes/auth.js:10` The login endpoint has no rate limiting, attempt counting, or lockout: ```js router.post('/login', async (req, res) => { const { username, password } = req.body; // Direct DB lookup, unlimited attempts ``` **Impact:** An attacker can run unlimited password guesses against any account at full network speed. With the default credentials documented in the README and displayed in the UI (see F-2), admin accounts are a trivial target. **Fix:** Apply `express-rate-limit` to the login route: ```js const rateLimit = require('express-rate-limit'); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 }); router.post('/login', loginLimiter, async (req, res) => { ... }); ``` --- ### C-5 — Default Credentials Displayed in Login UI **File:** `frontend/src/components/LoginForm.js:104` The login form renders hardcoded credentials in plain text: ```jsx

Default: admin / admin123

``` **Impact:** Anyone who opens the login page — including unauthenticated users — sees the default admin credentials. Combined with C-4 (no rate limiting), this is a direct path to admin compromise if the password has not been changed. **Fix:** Remove this block entirely. Document default credentials only in the deployment guide. Enforce password change on first login server-side. --- ### C-6 — Missing Sandbox Attribute on Knowledge Base PDF Iframe **File:** `frontend/src/components/KnowledgeBaseViewer.js:195` The inline document viewer renders uploaded files in an unsandboxed iframe: ```jsx