18 KiB
Security Audit - 2026-03-15
Scope
Static security review of the current CapaKraken codebase, focused on:
- authentication and authorization boundaries
- sensitive read/write API routes
- browser-exposed endpoints and SSE
- spreadsheet import surfaces
- secret handling and operational scripts
- dependency advisories from
pnpm audit --json
This review was done by parallel audit slices across API routes, auth/session code, frontend/browser entry points, import code, and dependency/config hygiene. It is a code audit, not a full penetration test.
Executive Summary
The main security problem is not one isolated bug. It is that CapaKraken currently treats "authenticated" as broadly equivalent to "allowed to see most planning data". That shows up in four places:
- any signed-in user can currently create a vacation request for any
resourceId - many sensitive read routes are only protected by
protectedProcedure - the SSE timeline feed broadcasts global planning events to every signed-in user
- untrusted spreadsheet parsing still relies on vulnerable
xlsx@0.18.5
Findings
High: Any authenticated user can create vacation requests for arbitrary resources
Evidence
packages/api/src/router/vacation.ts:140-207createis onlyprotectedProcedure- input accepts any
resourceId - the handler verifies the caller exists as a user, but does not verify ownership of the target resource
Impact
Any authenticated user can submit PENDING vacation requests on behalf of other employees. That is an authorization flaw with direct workflow impact, notification side effects, and possible HR/process abuse.
Recommended fix
- For
USERrole, requireresource.userId === ctx.dbUser.id - Allow cross-resource creation only for
MANAGER/ADMIN - Add an audit log entry containing actor and target resource
- Add tests for:
- user can create own vacation
- user cannot create vacation for another resource
- manager can create for another resource
High: Broad org and PII exposure behind protectedProcedure
Evidence
packages/api/src/trpc.ts:57-70protectedProcedureonly checksctx.session?.user- it does not require
ctx.dbUser
Representative routes exposed to any authenticated session:
packages/api/src/router/user.ts:18-29returns all users withname,email,systemRolepackages/api/src/router/resource.ts:50-292supports listing and searching resources bydisplayName,eid,emailpackages/api/src/router/project.ts:16-78lists all projectspackages/api/src/router/project.ts:80-101returns project plus allocations/demands/assignmentspackages/api/src/router/allocation.ts:165-299exposes allocations, demands, and assignmentspackages/api/src/router/vacation.ts:81-132exposes vacation records, requester, approver, and resource linkspackages/api/src/router/dashboard.ts:13-61exposes overview, peak times, demand analytics to any signed-in user
Impact
- least-privilege is not enforced for planning, staffing, and people data
- stale sessions can still pass
protectedProcedureeven if the backing DB user record is missing - authenticated users can enumerate internal users, resource identities, staffing status, vacations, and project demand
Recommended fix
- Introduce a stricter base procedure that requires both
session.useranddbUser - Reclassify read routes by data sensitivity:
adminProcedurefor user administrationmanagerProcedureorcontrollerProcedurefor staffing/financial/planning views- narrow self-service procedures for ordinary users
- Add field-level redaction for lower-privilege roles
- Define and document a route-by-route access matrix
High: Global SSE fan-out leaks planning events across users
Evidence
apps/web/src/app/api/sse/timeline/route.ts:8-60- any authenticated session can subscribe
packages/api/src/sse/event-bus.ts:117-145EventBus.subscribe()adds subscribers to a global set with no user, role, chapter, or project scopingpackages/api/src/sse/event-bus.ts:176-210- allocation, vacation, role, budget, and notification events are broadcast globally
Impact
Any signed-in user connected to the timeline SSE endpoint can receive metadata about organization-wide planning changes. Even when payloads are small, projectId, resourceId, notificationId, vacation status changes, and timing metadata are still sensitive internal signals.
Recommended fix
- Scope subscriptions per user and per permission set
- Filter event delivery before enqueueing into the stream
- Split channels by event type and audience, for example:
- personal notifications
- chapter-scoped planning updates
- manager/controller-only global planning updates
- Add contract tests to ensure a standard user does not receive unrelated events
High: Vulnerable spreadsheet parser used on untrusted input
Evidence
pnpm audit --jsonreports:xlsxprototype pollution advisoryGHSA-4r6h-8v6p-xvw6xlsxReDoS advisoryGHSA-5pgg-2g8v-p4x9
packages/application/src/use-cases/dispo-import/read-workbook.ts:22-41apps/web/src/lib/excel.ts:7-35apps/web/src/lib/skillMatrixParser.ts:85-124
Impact
CapaKraken parses spreadsheet data from files, including browser-side and import-related flows, with a library version that has known high-severity issues when reading crafted workbooks. Export-only flows are lower risk; read/parse flows are the real problem.
Recommended fix
- Replace
xlsxfor untrusted parsing, or isolate parsing into a hardened service/container - Enforce strict upload limits:
- max file size
- max sheet count
- max row/column count
- parse timeout / worker isolation
- Reject unsupported formats early
- Treat import parsing as untrusted-content processing, not as a normal request path
Medium: Secrets are stored in application settings and raw diagnostics are returned
Evidence
packages/api/src/router/settings.ts:50-140writesazureOpenAiApiKeyandsmtpPassworddirectly intosystemSettingspackages/api/src/router/settings.ts:142-225returnsrawAI connection errorspackages/api/src/lib/email.ts:64-80returns raw SMTP verification errors
Impact
- secrets appear to be stored plaintext at rest unless database-level encryption exists outside the app
- raw provider/network errors can leak infrastructure details, hostnames, or provider responses into the admin UI
Recommended fix
- Move secrets to a proper secret manager or add application-level encryption at rest
- Return sanitized admin-facing error categories and log detailed diagnostics server-side only
- Restrict who can trigger connection tests and audit those actions
Medium: Missing rate limiting on authentication and admin connectivity tests
Evidence
apps/web/src/server/auth.ts:20-37performs credential verification with no visible throttle, lockout, or rate limitingpackages/api/src/router/settings.ts:142-229exposes outbound AI and SMTP test actions to admins with no visible throttling- repository search found no application-level rate limiter for auth or admin test endpoints
Impact
- increased brute-force and credential-stuffing risk on login
- increased abuse and outbound scanning risk on AI/SMTP test functions
Recommended fix
- Add IP and account-based rate limiting to sign-in
- Add short cooldowns and audit logging to admin test endpoints
- Consider temporary lockout or progressive backoff for failed login attempts
Medium: Self-service skill matrix import has weak abuse controls
Evidence
packages/api/src/router/resource.ts:573-610- endpoint is only
protectedProcedure - client can submit arbitrary-length
skillsarrays and employee metadata
Impact
The caller is limited to their linked resource, which is good, but the endpoint still permits large user-controlled JSON payloads without explicit payload size caps, skill count caps, or normalization safeguards. That creates storage bloat and performance/availability risk, and can amplify downstream rendering problems.
Recommended fix
- Add maximum skill count and per-field length caps
- Reject duplicate or oversized payloads
- Normalize and sanitize string fields before persistence
- Add request body size limits on the transport layer
Medium: Reset script uses unsafe raw SQL and weak default bootstrap credentials
Evidence
packages/db/src/reset-dispo-import.ts:24-31defaults toadmin@capakraken.dev/admin123packages/db/src/reset-dispo-import.ts:107-115usesprisma.$executeRawUnsafe(...)
Impact
This is operational tooling, not a normal app route, so the exposure is lower. But if used in the wrong environment, it can bootstrap a predictable admin credential and relies on unsafe SQL execution patterns.
Recommended fix
- Remove default admin password and require explicit credentials
- Keep the script restricted to local/admin-only environments
- Replace
$executeRawUnsafewith a safer pattern if practical, or add a hard guard that blocks execution outside approved environments
Low: Inline script in layout is static today, but CSP hardening is still missing
Evidence
apps/web/src/app/layout.tsx:27-33usesdangerouslySetInnerHTMLfor theme bootstrapping fromlocalStorage
Impact
The current snippet is static and does not interpolate user input, so this is not a major vulnerability by itself. The issue is that it keeps the app dependent on inline-script allowance unless a nonce/hash-based CSP is introduced.
Recommended fix
- Add a Content Security Policy
- Move to nonce or hash-based allowance for this bootstrap script
- Keep inline script content static and minimal
Low: Dependency advisories also exist in tooling/dev chain
Evidence
pnpm audit --json additionally reports:
esbuildmoderate advisory in the Vite/Vitest chainflattedhigh advisory through the ESLint toolchain
Impact
These are materially lower priority than the xlsx parser issue because they appear in development/tooling paths rather than the primary production import surface. They still deserve cleanup to keep local developer environments safer and reduce future drift.
Recommended fix
- upgrade
esbuildthrough Vite/Vitest where possible - upgrade
flattedvia the ESLint dependency chain - keep dependency audit in CI so new advisories are caught early
Additional Findings (Independent Audit — 2026-03-15)
A second independent audit identified the following issues not covered above.
Critical: Vacation cancel endpoint has no ownership check (IDOR)
Evidence
packages/api/src/router/vacation.ts:346-364cancelusesprotectedProcedure- no check that the caller owns the vacation or the associated resource
Impact
Any authenticated user can cancel APPROVED or PENDING vacations for any employee by supplying the vacation ID. This is distinct from the vacation-create IDOR above and has equal or greater impact since it can undo already-approved time-off.
Recommended fix
- For
USERrole, verifyexisting.requestedById === ctx.dbUser.idorresource.userId === ctx.dbUser.id - Allow cross-resource cancellation only for
MANAGER/ADMIN
High: No security headers (X-Frame-Options, HSTS, Referrer-Policy)
Evidence
apps/web/next.config.tscontains noheaders()configuration- no
helmetor equivalent middleware found
Impact
Clickjacking (missing X-Frame-Options), protocol downgrade (missing HSTS), information leakage (missing Referrer-Policy), MIME sniffing (missing X-Content-Type-Options).
Recommended fix
Add a headers() function to next.config.ts returning X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and Strict-Transport-Security for production.
High: Weak password policy
Evidence
packages/api/src/router/user.ts:56— password validation isz.string().min(8)apps/web/src/server/auth.ts:9— login acceptsz.string().min(1)- seed uses
admin123,manager123,viewer123
Impact
Combined with the lack of rate limiting, weak passwords are easily brute-forced.
Recommended fix
Strengthen password schema to require minimum 12 characters with mixed case, digits, and special characters. Add a password change flow.
High: Audit log entries missing user attribution
Evidence
packages/api/src/router/allocation.ts,project.ts,role.ts—auditLog.createcalls do not setuserId- the
AuditLogPrisma model has auserIdfield, but router code does not populate it
Impact
Audit trails cannot attribute actions to specific users. Insider threats are untraceable.
Recommended fix
Always include userId: ctx.dbUser?.id in every auditLog.create call. Consider middleware-based automatic attribution.
High: user.list exposes all users to any authenticated user
Evidence
packages/api/src/router/user.ts:18-29—protectedProcedure- returns
name,email,systemRole,createdAtfor all users
Impact
Full user directory enumeration by any authenticated account including VIEWER role. Facilitates social engineering.
Recommended fix
Restrict to adminProcedure or managerProcedure.
Medium: No Next.js middleware for route protection
Evidence
- no
middleware.tsexists inapps/web/src/
Impact
Authentication is only enforced at the API/tRPC layer. Page shells and layouts render for unauthenticated requests before client-side auth kicks in.
Recommended fix
Add middleware.ts that redirects unauthenticated users to /auth/signin for all routes under /(app)/.
Medium: Blueprint updateRolePresets accepts unvalidated JSON
Evidence
packages/api/src/router/blueprint.ts:72—rolePresets: z.array(z.unknown())
Impact
Arbitrary JSON stored in JSONB field. Could cause rendering errors or storage bloat.
Recommended fix
Define at minimum a basic shape schema. Add serialized size check.
Medium: Docker Compose hardcodes NEXTAUTH_SECRET
Evidence
docker-compose.yml:46-49—NEXTAUTH_SECRET: dev-secret-change-in-production
Impact
If used in non-dev environments, sessions can be forged with the known secret.
Recommended fix
Reference environment variable: NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}. Add startup check.
Medium: Entitlement balance accessible without ownership check
Evidence
packages/api/src/router/entitlement.ts:103-145—protectedProcedure, accepts anyresourceId
Impact
Any authenticated user can query vacation balances for any employee.
Recommended fix
For USER-role callers, restrict to their own linked resource.
Medium: Email content not sanitized for HTML injection
Evidence
packages/api/src/lib/email.ts—htmlparameter available inEmailPayloadrejectionReasonfield included in email body without sanitization
Impact
If HTML emails are ever sent, user-supplied rejection reasons could contain executable HTML.
Recommended fix
Apply HTML escaping to all user-supplied text in emails.
Low: JWT session strategy without explicit maxAge or rotation
Evidence
apps/web/src/server/auth.ts:60-62—strategy: "jwt"with nomaxAge
Impact
Stolen JWTs remain valid for 30 days (Auth.js default). No session revocation mechanism.
Recommended fix
Set shorter maxAge (e.g., 24 hours). Implement token rotation.
Low: tRPC error formatter exposes Zod validation details
Evidence
packages/api/src/trpc.ts:34-44—zodErrorflattened in error responses
Impact
Internal field names and validation rules leak to clients.
Recommended fix
Conditionally include zodError only when NODE_ENV=development.
Low: Redis connection without authentication
Evidence
packages/api/src/sse/event-bus.ts:80—redis://localhost:6380with no password- Docker Compose Redis has no
requirepass
Impact
In production, unauthenticated Redis can be accessed by network neighbors.
Recommended fix
Configure Redis with requirepass in production.
Low: user.setPermissions accepts arbitrary string arrays
Evidence
packages/api/src/router/user.ts:116-135—granted: z.array(z.string())
Impact
Invalid permission keys can be stored in the database.
Recommended fix
Use z.array(z.enum([...PermissionKey values])) to validate known keys.
Positive findings
- Argon2 password hashing via
@node-rs/argon2 - All write operations gated behind
managerProcedureoradminProcedure; nopublicProcedurein routers - Prisma ORM prevents SQL injection (no raw SQL in app routes)
- Comprehensive anonymization system consistently applied across endpoints
- Zod input validation on all tRPC endpoints
.envfiles properly gitignored- SSE endpoint requires authentication (lacks filtering)
- Report endpoint applies anonymization
Recommended Remediation Order
Immediate
- Fix vacation ownership checks
- Lock down the SSE timeline feed by audience/permission
- Require
dbUserfor all protected access - Tighten the most sensitive read routes (
user,vacation,allocation, project detail)
Next
- Replace or isolate
xlsxfor all untrusted parsing - Add auth/admin rate limiting
- Sanitize admin diagnostics and improve secret storage
- Add limits to skill matrix import payloads
After That
- Harden the reset script
- Add CSP and browser hardening headers
- Clean up dev/tooling dependency advisories
Concrete Improvement Backlog
- Build a permission matrix for every tRPC procedure
- Introduce
authenticatedDbUserProcedureas the new default - Separate self-service APIs from planning/admin APIs
- Add structured audit logging for sensitive reads and admin test actions
- Add request size limits for upload/import endpoints
- Add CI checks for
pnpm audit, secret scanning, and security regression tests
Suggested Verification After Fixes
- authorization tests for every role against high-risk routes
- SSE tests proving event isolation by role/user
- import fuzz tests with malformed and oversized spreadsheets
- login throttling tests
- regression tests for vacation ownership rules
Bottom Line
The codebase is not in a catastrophic state, but it is currently too trusting of any authenticated session. The most important improvements are authorization narrowing, event-stream scoping, and safer spreadsheet import handling.