16 KiB
Security Audit Tracker — STEAM Security Dashboard
Last scan: 2026-04-20
Scope: Full repository — backend routes, middleware, helpers, scripts, frontend components
Baseline: docs/security-audit-2026-04-01.md (31 findings), docs/security-remediation-plan.md (17 prioritised items)
Table of Contents
- Remediation Status — April 1 Audit
- New Findings — April 20 Scan
- Open Finding Summary
- Positive Security Observations
- Scan Metadata
Remediation Status — April 1 Audit
Cross-reference of the 31 original findings against the current codebase. Status: Fixed, Partial, or Open.
Critical Findings
| ID | Title | Status | Evidence |
|---|---|---|---|
| C-1 | Missing auth on Ivanti findings endpoints | Fixed | ivantiFindings.js — router uses requireAuth(db) at router level, requireGroup on sync |
| C-2 | requireRole(db) bypasses role check in KB routes |
Fixed | knowledgeBase.js — uses requireGroup('Admin', 'Standard_User') correctly |
| C-3 | Unauthenticated finding note writes | Fixed | ivantiFindings.js — note routes behind requireAuth(db) |
| C-4 | No brute force protection on login | Fixed | auth.js — loginLimiter (20 attempts / 15 min) applied to POST /login |
| C-5 | Default credentials displayed in login UI | Fixed | LoginForm.js — no hardcoded credentials in the component |
| C-6 | Missing sandbox on KB document iframe | Fixed | KnowledgeBaseViewer.js:282 — sandbox="allow-same-origin" applied |
High Findings
| ID | Title | Status | Evidence |
|---|---|---|---|
| H-1 | /cleanup-sessions missing role check |
Fixed | auth.js — requireAuth(db), requireGroup('Admin') applied |
| H-2 | Hardcoded fallback SESSION_SECRET | Fixed | server.js:34-37 — hard-fails with process.exit(1) if unset |
| H-3 | Audit log parameter mismatch — silent trail gaps | Partial | knowledgeBase.js — fixed. archerTickets.js — logAudit calls missing username field (see N-1 below) |
| H-4 | Viewers can write compliance notes | Fixed | compliance.js — requireGroup('Admin', 'Standard_User') on POST /notes |
| H-5 | Sync endpoints accessible to all authenticated users | Fixed | Both ivantiFindings.js and ivantiWorkflows.js — requireGroup('Admin', 'Standard_User') on POST /sync |
| H-6 | HTTP header injection via Content-Disposition filename | Fixed | knowledgeBase.js — filename sanitized with .replace(/["\r\n\\]/g, '') |
| H-7 | Race condition in KB file upload | Fixed | knowledgeBase.js — file moved after DB insert succeeds |
| H-8 | Hardcoded default admin password in setup.js | Fixed | setup.js — generates random password via crypto.randomBytes(12) |
| H-9 | ReactMarkdown renders HTML without sanitization | Fixed | KnowledgeBaseViewer.js — rehypeSanitize plugin applied |
Medium Findings
| ID | Title | Status | Evidence |
|---|---|---|---|
| M-1 | No CSRF token protection | Open | Cookies use SameSite: lax — no CSRF token implemented |
| M-2 | CORS credentials with explicit origin list | Open | Acceptable for this deployment model — monitor |
| M-3 | No rate limiting on NVD API proxy | Open | No server-side cache or per-user rate limit on /api/nvd/lookup |
| M-4 | Admin self-demotion check uses loose equality | Fixed | users.js — uses String(userId) === String(req.user.id) |
| M-5 | Missing hostname format validation | Fixed | compliance.js POST /notes — regex validation ^[a-zA-Z0-9._-]+$ |
| M-6 | Vendor field validated before trim | Open | ivantiTodoQueue.js:8 — isValidVendor() checks length before trim |
| M-7 | Unsanitized original filename in temp JSON | Open | compliance.js:344 — req.file.originalname passed directly |
| M-8 | Hardcoded frontend IP in CSP header | Fixed | knowledgeBase.js:302 — reads from CORS_ORIGINS env var |
| M-9 | API error messages forwarded to UI | Open | Frontend still uses alert(err.message) in several places |
| M-10 | User data in window.confirm dialogs | Open | Frontend still uses window.confirm with user-supplied data |
Low / Info Findings
| ID | Title | Status | Evidence |
|---|---|---|---|
| L-1 | Silent ROLLBACK on transaction failure | Open | compliance.js:167 — .catch(() => {}) still swallows errors |
| L-2 | Fire-and-forget audit logging | Partial | auditLog.js — now logs to console.error on failure, but no alerting |
| L-3 | Async temp file cleanup with no error handling | Open | compliance.js — fs.unlink(path, () => {}) still used |
| L-4 | IVANTI_SKIP_TLS with no startup warning | Open | No startup warning when IVANTI_SKIP_TLS=true |
| L-5 | console.error in production frontend | Open | No environment guard on console.error calls |
| L-6 | localStorage column config lacks structural validation | Open | No change observed |
Remediation Plan Items (not in original 31)
| ID | Title | Status | Evidence |
|---|---|---|---|
| RP-1 | Authenticate /uploads static file access | Open | server.js:127 — express.static('uploads') still unauthenticated |
| RP-2 | Sanitize Mermaid SVG output with DOMPurify | Open | KnowledgeBaseViewer.js:38 — innerHTML = svg without DOMPurify |
| RP-3 | Strip server file paths from compliance preview response | Open | compliance.js:342 — full tempFilePath returned to client |
| RP-4 | Add SESSION_SECRET to .env.example | Open | .env.example — no SESSION_SECRET entry |
New Findings — April 20 Scan
Findings discovered in this scan that were not present in the April 1 audit.
N-1 — Archer Ticket Audit Logs Missing username Field (Medium)
File: backend/routes/archerTickets.js:89, 172, 195
All three logAudit calls in the Archer tickets router omit the username field:
logAudit(db, {
userId: req.user.id,
action: 'CREATE_ARCHER_TICKET',
// username: req.user.username ← missing
...
});
The auditLog.js helper defaults missing username to 'unknown', so all Archer ticket audit entries show username = 'unknown' instead of the actual user.
Impact: Audit trail for Archer ticket operations cannot identify which user performed the action. Compliance reviews and incident investigations are degraded.
Fix: Add username: req.user.username to all three logAudit calls.
N-2 — migrate-to-1.1.js Contains Hardcoded Admin Password (Medium)
File: backend/migrate-to-1.1.js:246
const passwordHash = await bcrypt.hash('admin123', 10);
While setup.js was fixed to generate random passwords (H-8), the migration script still hardcodes admin123. If this migration is run on an existing deployment, it resets the admin password to a known value.
Impact: Running the migration on a production system resets the admin account to a publicly known password.
Fix: Either generate a random password (matching setup.js pattern) or skip admin creation if the user already exists.
N-3 — Compliance Preview Returns Full Server Filesystem Path (Medium)
File: backend/routes/compliance.js:342
tempFile: tempFilePath,
The preview endpoint returns the full server-side path (e.g. /home/cve-dashboard/backend/uploads/temp/compliance_preview_...json) to the frontend. The commit endpoint then receives this path back and reads the file. This exposes the server's directory structure to any authenticated user.
Impact: Information disclosure — authenticated users learn the server's absolute filesystem layout, which aids further exploitation.
Fix: Return only the filename. Reconstruct the full path server-side in the commit handler:
tempFile: tempFilename, // just the basename
// In commit handler:
const tempFile = path.join(TEMP_DIR, path.basename(req.body.tempFile));
N-4 — /uploads Static Directory Served Without Authentication (High)
File: backend/server.js:127
app.use('/uploads', express.static('uploads', {
dotfiles: 'deny',
index: false
}));
All uploaded files (CVE documents, compliance data, knowledge base articles) are served as static files without any authentication check. Anyone who knows or guesses a file URL can access sensitive vulnerability documentation, compliance reports, and internal knowledge base content.
Impact: Unauthenticated access to all uploaded documents. File paths are predictable (CVE ID + vendor + timestamp-filename pattern).
Fix: Replace with an authenticated route handler:
app.use('/uploads', requireAuth(db), express.static('uploads', { ... }));
N-5 — Mermaid SVG Rendered via innerHTML Without Sanitization (Medium)
File: frontend/src/components/KnowledgeBaseViewer.js:38
ref.current.innerHTML = svg;
Mermaid-generated SVG is injected directly into the DOM via innerHTML. While Mermaid itself sanitizes most input, a crafted diagram definition in a knowledge base article could potentially produce SVG with embedded event handlers or script elements.
Impact: Stored XSS vector if Mermaid's internal sanitization is bypassed. Any user viewing the article would execute the payload.
Fix: Sanitize the SVG string before injection:
import DOMPurify from 'dompurify';
ref.current.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true } });
N-6 — SESSION_SECRET Not Documented in .env.example (Low)
File: backend/.env.example
The SESSION_SECRET environment variable is required for the server to start (hard-fail added per H-2 fix), but it is not listed in .env.example. Fresh deployments will fail with no guidance on what to set.
Fix: Add to .env.example:
# Session signing secret — generate with: openssl rand -hex 32
SESSION_SECRET=
N-7 — requireGroup Error Response Leaks Current User Group (Low)
File: backend/middleware/auth.js:55-60
return res.status(403).json({
error: 'Insufficient permissions',
required: allowedGroups,
current: req.user.group
});
The 403 response includes both the required groups and the user's current group. This is minor information disclosure — an attacker probing endpoints learns the exact group membership of the compromised account and which groups are needed.
Fix: Remove required and current from the response:
return res.status(403).json({ error: 'Insufficient permissions' });
N-8 — No Content-Security-Policy Header on Main Application (Medium)
File: backend/server.js:107-113
Security headers include X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, and Permissions-Policy, but no Content-Security-Policy header. CSP is the primary browser-side defense against XSS.
Impact: No browser-enforced restriction on script sources. If an XSS vulnerability exists (e.g. N-5), there is no CSP to mitigate it.
Fix: Add a baseline CSP header:
res.setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; font-src 'self'; connect-src 'self'");
Start with Content-Security-Policy-Report-Only to avoid breaking existing functionality.
N-9 — Expired Sessions Not Cleaned Up Automatically (Low)
File: backend/server.js, backend/routes/auth.js
The sessions table has no automatic cleanup. Expired sessions accumulate indefinitely. The /cleanup-sessions endpoint exists but must be triggered manually by an admin.
Impact: Performance degradation over time as the sessions table grows. Not directly exploitable, but expired session rows increase the surface for timing attacks on session lookups.
Fix: Add a cleanup interval on server startup:
setInterval(() => {
db.run("DELETE FROM sessions WHERE expires_at < datetime('now')");
}, 6 * 60 * 60 * 1000); // every 6 hours
Open Finding Summary
Prioritised list of all open findings requiring action.
High Priority
| ID | Severity | Title | Source |
|---|---|---|---|
| N-4 | High | /uploads static directory served without authentication |
New |
Medium Priority
| ID | Severity | Title | Source |
|---|---|---|---|
| M-1 | Medium | No CSRF token protection | April 1 |
| M-3 | Medium | No rate limiting on NVD API proxy | April 1 |
| N-1 | Medium | Archer ticket audit logs missing username field |
New |
| N-2 | Medium | migrate-to-1.1.js contains hardcoded admin password |
New |
| N-3 | Medium | Compliance preview returns full server filesystem path | New |
| N-5 | Medium | Mermaid SVG rendered via innerHTML without sanitization |
New |
| N-8 | Medium | No Content-Security-Policy header on main application | New |
| M-6 | Medium | Vendor field validated before trim | April 1 |
| M-7 | Medium | Unsanitized original filename in temp JSON | April 1 |
| M-9 | Medium | API error messages forwarded to UI | April 1 |
| M-10 | Medium | User data in window.confirm dialogs |
April 1 |
Low Priority
| ID | Severity | Title | Source |
|---|---|---|---|
| N-6 | Low | SESSION_SECRET not documented in .env.example |
New |
| N-7 | Low | requireGroup error response leaks current user group |
New |
| N-9 | Low | Expired sessions not cleaned up automatically | New |
| L-1 | Low | Silent ROLLBACK on transaction failure | April 1 |
| L-3 | Low | Async temp file cleanup with no error handling | April 1 |
| L-4 | Low | IVANTI_SKIP_TLS with no startup warning | April 1 |
| L-5 | Low | console.error in production frontend | April 1 |
| L-6 | Low | localStorage column config lacks structural validation | April 1 |
Positive Security Observations
Verified secure patterns that should be preserved:
- SQL injection prevention — all queries use parameterized statements throughout the entire codebase
- Path traversal prevention —
sanitizePathSegment()andisPathWithinUploads()consistently applied inserver.js,compliance.js, andknowledgeBase.js - Python script execution —
spawn('python3', [SCRIPT, filePath])with argument arrays — no shell injection - File upload security — extension allowlist + MIME prefix validation + 10 MB size limit via multer
- Password hashing — bcrypt with cost factor 10 used for all password storage
- Session management — 32-byte random session IDs via
crypto.randomBytes, httpOnly cookies, 24h expiry - Rate limiting — login endpoint protected with 20 attempts per 15-minute window
- Audit trail — comprehensive audit logging on all state-changing operations (with noted exceptions above)
- Self-modification prevention — admin cannot demote or deactivate their own account
- Ownership-scoped deletion — Standard_User can only delete resources they created
- Compliance linkage protection — deletion blocked when tickets are linked to active compliance reports
- Temp file path validation —
isSafeTempPath()enforces.jsonextension anduploads/temp/directory - Static file serving —
dotfiles: 'deny'andindex: falseprevent directory listing
Scan Metadata
| Field | Value |
|---|---|
| Scan date | 2026-04-20 |
| Scan type | Full repository static analysis |
| Scope | backend/, frontend/src/, config files |
| Baseline | docs/security-audit-2026-04-01.md |
| Previous findings | 31 (6 Critical, 9 High, 10 Medium, 6 Low/Info) |
| Remediated | 20 fully fixed, 2 partially fixed |
| Still open (from baseline) | 13 |
| New findings | 9 |
| Total open | 22 (1 High, 11 Medium, 10 Low) |
| Methodology | Static analysis — code review of all route handlers, middleware, helpers, and frontend components |