19 KiB
Phase 6 - Security Audit
Executive Summary
The application has a solid baseline in two important areas: server-side authorization is used broadly across route handlers, and many sensitive workflow actions are audited. The main security risks are not widespread missing auth checks, but a smaller set of high-impact gaps in file delivery, request hardening, dependency posture, and abuse protection.
The most serious application-layer issue is direct public exposure of uploaded online-lesson assets. Uploaded video, attachment, and thumbnail files are stored under public/uploads/..., serialized back to clients as direct public URLs, and rendered directly in the UI. This bypasses the guarded download pattern that already exists in other parts of the system.
The second major risk is the import surface. Both spreadsheet import endpoints accept files based only on extension presence, read the entire file into memory, and rely on xlsx, which npm audit currently reports with high-severity advisories. Combined with the absence of rate limiting and explicit file-size controls on import endpoints, this increases the risk of denial-of-service and unsafe spreadsheet parsing.
Threat Surface Inventory
Primary security-sensitive surfaces reviewed:
- Auth.js credential login flow
- Session and active-organization switching
- Server-side authorization helpers
- Permission-management APIs
- Announcement, online-lesson, and certificate file handling
- Training-record and employee spreadsheet imports
- Report export endpoints
- Notification and workflow action endpoints
- Next.js runtime/configuration and proxy perimeter
Authentication Assessment
Strengths
src/auth.ts:25-33validates login input and uses bcrypt password verification.src/auth.ts:57,81,103,137records login success and failure events in audit logs.- Self-service registration is disabled in
src/app/api/auth/register/route.ts. - Session enforcement is centralized through
requireSession().
Findings
src/auth.ts:33falls back todev-only-auth-secret-change-meoutside production. This is acceptable for local development but risky if a non-production environment is internet-exposed with weak secret hygiene.src/auth.ts:36setstrustHost: true. This is common in proxied deployments, but it raises deployment sensitivity if host/header forwarding is not tightly controlled upstream.- No brute-force controls, lockout logic, or request throttling were found around credential login.
- Password policy is minimal: sign-in and auth forms enforce only
min(8), andsrc/features/users/schemas/user.tsdoes not enforce password strength itself.
Assessment
Authentication is functionally correct, but it lacks abuse resistance and stronger operational guardrails.
Authorization Assessment
Strengths
- Broad route coverage uses server-side guards such as
requireOrganizationAccess,requirePermission,requireHRD,requireAllPermissions, andrequireSuperAdminOrganizationAccess. - Permission-management endpoints are protected at the route-handler level even though they are not included in the proxy perimeter list.
- Download routes for announcement attachments and training-record certificates enforce organization-aware access checks before reading files from disk.
Findings
src/proxy.ts:4-18,41-56only protects a subset of API prefixes at the perimeter. Notably, online-lesson and permission-management APIs are not listed there, even though their handlers do perform server-side checks. This is a defense-in-depth gap rather than a direct bypass.src/lib/auth/session.tsandsrc/app/api/organizations/active/route.tscan auto-create admin memberships for super-admin/HRD organization access. This may be intended, but it is a privileged side effect inside an access path and should be treated carefully.- Permission-management routes use
requireSuperAdminOrganizationAccess(), which currently depends on the same organization-access helper that can create fallback memberships.
Assessment
Authorization is stronger than average for an admin dashboard. The main risk is not broken route protection by default, but privileged side effects embedded inside access-resolution flows.
Input Validation Assessment
Strengths
- Core workflow routes use Zod or equivalent server-side validation.
- Online-lesson URLs are constrained to
http/httpsvia schema validation. - Route params are commonly parsed and rejected when numeric IDs are invalid.
Findings
src/app/api/training-records/import/route.ts:228-236andsrc/app/api/import-employees/route.ts:460-474only verify that the upload is aFileand that the name ends with.xlsx; no explicit file-size or MIME validation is enforced before parsing.- Import endpoints load the full spreadsheet with
await file.arrayBuffer()before further validation. - Several workflow routes perform large amounts of procedural validation inline inside route handlers, which makes consistency harder to maintain.
Assessment
Input validation is decent for routine CRUD, but weaker on large-file and batch-import surfaces.
Injection Assessment
Strengths
- Most database access uses Drizzle query builders and parameterized expressions.
- No command-execution patterns or child-process usage were found in application runtime paths.
- Storage helpers sanitize path segments and verify resolved paths stay under upload roots.
Findings
npm auditreports a high-severity advisory fordrizzle-ormregarding SQL injection via improperly escaped SQL identifiers in versions<0.45.2.src/features/reports/server/report-data.ts:446-555uses raw SQL composition for reporting. The visible usage appears parameterized, but this area should be considered high-scrutiny because it is the most SQL-heavy path in the app.npm auditreports high-severity advisories forxlsx, which is used to parse untrusted spreadsheet uploads.- CSV export exists in
src/app/api/audit-logs/export/route.ts; this phase did not fully verify whether cells beginning with spreadsheet formula prefixes are neutralized.
Assessment
There is no clear app-authored SQL injection bug in the reviewed code, but dependency-level injection risk is real and currently unresolved.
XSS Assessment
Strengths
- Announcement content, review comments, and online-lesson descriptions are rendered as plain text with
whitespace-pre-wrap, not injected HTML. - No user-content Markdown or rich-text renderer was found in the reviewed content workflows.
- The single
dangerouslySetInnerHTMLusage insrc/components/ui/chart.tsxis used to emit generated CSS variables, not user-provided rich text.
Findings
- Online-lesson external video URLs are rendered into outbound links and embed decisions. The code restricts embeddable iframes to recognized YouTube hosts, which is good, but general outbound URLs are still allowed for non-embeddable links.
- No repo-level Content Security Policy was found in
next.config.tsor related runtime config.
Assessment
Direct stored-XSS exposure appears low in the reviewed flows. The larger XSS concern is missing browser-side hardening such as CSP.
CSRF & Request Protection Assessment
Strengths
- State-changing operations generally use non-GET methods.
- Auth is session-based and routed through Auth.js rather than custom ad hoc cookies.
Findings
- No explicit CSRF token validation, origin checks, or referer checks were found in state-changing route handlers.
- No centralized request-hardening middleware was found for mutation endpoints.
- Because cookie behavior is library-managed, SameSite/secure cookie posture was not explicitly verified from local code.
Assessment
This is primarily a hardening gap, not a confirmed exploit in the reviewed code. Still, the app currently relies heavily on framework defaults and browser cookie behavior rather than explicit anti-CSRF controls.
File Upload Assessment
High-risk finding
src/lib/online-lesson-storage.ts:36-86writes uploaded lesson assets topublic/uploads/online-lessons/...and returns direct public URLs.src/features/online-lessons/server/online-lesson-data.ts:51,53,55serializes those direct file URLs to clients unchanged.src/features/online-lessons/components/online-lesson-detail-page.tsx:198-200,284-290,328-336renders those URLs directly in<img>,<video>, and download/open links.
Impact:
- Any user or external party who obtains one of these URLs can fetch the file directly from the public assets path without passing through authorization checks.
- This creates a real broken-access-control and data-exposure risk for uploaded lesson media and attachments.
Additional findings
src/lib/announcement-storage.tsandsrc/lib/certificate-storage.tsalso store files underpublic/uploads/...and create direct public paths, but the primary application serializers now prefer guarded API download URLs instead of returning the raw storage path to clients.- No virus scanning, content scanning, or magic-byte verification was found for uploaded files.
- Online-lesson attachments permit several document/archive formats, including ZIP, but are still served from public paths.
Assessment
File upload handling is the most important application-layer security weakness in the current codebase, especially for online-lesson assets.
Sensitive Data Assessment
Strengths
- Many protected download responses use
Cache-Control: private, no-store. - Permission and organization access is checked server-side before reading many sensitive records.
Findings
- Uploaded lesson assets are publicly reachable by URL, which can expose internal training content and attachments.
- Login failure audit logs in
src/auth.tspersist identifiers and, in one case, user email inside audit payloads. This may be acceptable operationally, but it increases PII footprint inside logs. - User and employee tables render email, employee code, organization, and department information broadly across admin UIs; this is expected for the app, but it increases the need for stronger export and audit controls.
Assessment
The biggest sensitive-data problem is file exposure, not routine JSON overexposure in the reviewed API routes.
Security Headers Assessment
Findings
- No
headers()configuration was found innext.config.ts. - No explicit Content Security Policy,
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy, or HSTS configuration was found in reviewed app config. - Download routes set
Cache-Control, but broader site-level browser hardening appears absent.
Assessment
Browser-side security headers are currently under-configured.
Rate Limiting Assessment
Findings
- No rate-limiting or throttling implementation was found for login, imports, exports, uploads, or workflow actions.
- This directly affects:
- Credential login
- Spreadsheet import endpoints
- Report export endpoint
- File upload endpoints
- Notification mutation endpoints
Assessment
Rate limiting is a notable gap and materially increases brute-force and denial-of-service risk.
Audit Log Assessment
Strengths
- Authentication success/failure is logged.
- Permission changes have dedicated audit infrastructure.
- Import operations log audit entries.
- Announcement and workflow transitions show extensive audit logging.
Findings
src/app/api/reports/export/route.tsdoes not log report exports, even though exports can contain organization-wide training and employee data.- This creates an audit gap for sensitive data extraction.
- This phase did not verify logout logging or sensitive-record read auditing beyond the reviewed routes.
Assessment
Audit coverage is good for mutations, but weaker for data-export visibility.
Dependency & Configuration Assessment
npm audit results
- Total vulnerabilities:
11 - High:
3 - Moderate:
8
Key findings:
drizzle-orm- High severity advisory reported by
npm audit.
- High severity advisory reported by
xlsx- High severity advisories reported by
npm audit. - Especially relevant because the app parses untrusted spreadsheet uploads in two endpoints.
- High severity advisories reported by
sort-by/object-path- High/moderate advisory chain reported by
npm audit.
- High/moderate advisory chain reported by
@opentelemetry/coreand related packages- Moderate memory-allocation advisory reported by
npm audit.
- Moderate memory-allocation advisory reported by
Configuration findings
src/auth.ts:33uses a development fallback secret.src/auth.ts:36setstrustHost: true.next.config.tsremoves console logs in production, which is good, but does not add security headers.
Assessment
Dependency security posture needs attention now, especially because the vulnerable packages are in active code paths rather than unused tooling only.
Business Logic Security Assessment
Strengths
- Review/publish/archive flows check content status and permissions before transition.
- Training-record certificate download checks ownership/scope through the parent training record.
Findings
- Privileged organization switching can materialize new admin memberships as a side effect.
- Report exports are permission-checked, but not audited.
- This phase did not find a clear "publish without approve" bypass in the reviewed content workflows, but the workflow handlers are large enough that they deserve dedicated regression tests.
Assessment
Business-logic security is generally thoughtful, but several privileged flows remain too implicit and too large for easy verification.
Findings by Severity
High
-
Online-lesson uploaded assets are publicly accessible by direct URL.
-
Spreadsheet import endpoints accept unbounded
.xlsxuploads and parse them with a dependency that currently has high-severity advisories.- Evidence: training-record import route, employee import route,
npm auditfinding forxlsx
- Evidence: training-record import route, employee import route,
-
Active dependency vulnerabilities include high-severity advisories in
drizzle-ormandxlsx.- Evidence:
npm audit --json
- Evidence:
Medium
- No brute-force or rate-limiting protections were found for login, upload, import, or export endpoints.
- No repo-level security headers configuration was found.
- No explicit CSRF/origin validation layer was found for mutation endpoints.
- Report exports are not audit-logged.
- Privileged organization access can create admin memberships as a side effect.
- Login audit logs increase sensitive identifier/email footprint inside logs.
Low
- Proxy perimeter coverage is incomplete, though server-side route guards still protect the affected APIs.
- Development fallback secret is acceptable locally, but risky if misused in a shared environment.
Quick Wins
- Stop returning direct public URLs for online-lesson files; serve them through guarded download/stream routes.
- Add strict file-size limits and MIME checks to both spreadsheet import endpoints before
arrayBuffer()parsing. - Add rate limiting for
/api/auth/[...nextauth], import, export, and upload routes. - Add export audit logging for reports.
- Add baseline browser security headers in Next.js config or edge middleware.
Structural Improvements
- Move all sensitive file delivery to authenticated route handlers and eliminate
public/uploadsfor protected assets. - Introduce a centralized request-hardening layer for origin checking, abuse controls, and common mutation protections.
- Separate privileged organization-access resolution from membership-creation side effects.
- Add dedicated security tests for workflow transitions, file access, and export scope enforcement.
- Review and remediate vulnerable dependencies in a controlled upgrade track, especially
xlsxanddrizzle-orm.
Recommended Remediation Order
-
Online-lesson file exposure
- Highest real-world access-control risk.
-
Import surface hardening
- Add file-size checks and reduce exposure from vulnerable spreadsheet parsing.
-
Dependency remediation
- Prioritize packages in active parsing/data-access paths.
-
Rate limiting and request-hardening
- Best leverage across login, import, export, and uploads.
-
Security headers and audit-gap cleanup
- Important defense-in-depth after the higher-risk access issues are contained.
Commands Executed and Results
rgandSelect-Stringacross auth, proxy, storage, API routes, and config- Used to inspect auth guards, upload paths, security headers, and audit coverage.
npm audit --json- Reported
11vulnerabilities total:3high and8moderate.
- Reported
Files Reviewed
src/auth.tssrc/proxy.tssrc/lib/auth/session.tssrc/lib/auth/authorization.server.tsnext.config.tssrc/lib/announcement-storage.tssrc/lib/online-lesson-storage.tssrc/lib/certificate-storage.tssrc/app/api/auth/register/route.tssrc/app/api/organizations/active/route.tssrc/app/api/reports/export/route.tssrc/app/api/training-records/import/route.tssrc/app/api/import-employees/route.tssrc/app/api/announcements/[id]/attachment/route.tssrc/app/api/training-records/[id]/certificates/route.tssrc/app/api/training-records/[id]/certificates/[certificateId]/download/route.tssrc/app/api/user-permissions/route.tssrc/app/api/permission-templates/route.tssrc/app/api/permissions/catalog/route.tssrc/features/announcements/server/announcement-data.tssrc/features/online-lessons/server/online-lesson-data.tssrc/features/training-records/server/training-record-data.tssrc/features/online-lessons/schemas/online-lesson.tssrc/features/announcements/components/announcement-view-page.tsxsrc/features/online-lessons/components/online-lesson-detail-page.tsxsrc/components/ui/chart.tsxsrc/features/users/schemas/user.ts
Areas Not Verified
- This phase did not perform live penetration testing, browser interception, or runtime CSRF validation against a running deployment.
- Cookie flags were not verified from actual HTTP responses.
- This phase did not exhaustively review every route handler in the repository; it focused on the highest-risk auth, permission, upload, import, and export surfaces.
- Dependency advisories were taken from local
npm auditoutput and were not independently triaged against actual reachable exploit paths in this specific deployment model.