Skip to content

Conversation

@github-actions
Copy link
Contributor

This is an automated pull request to release the candidate branch into production, which will trigger a deployment.
It was created by the [Production PR] action.

github-actions bot and others added 2 commits November 24, 2025 23:18
…onnaire (#1828)

* refactor(security-questionnaire): transfer auto-answer functionality to SSE part

* refactor(security-questionnaire): simplify handling of originalIndex in components

* refactor(security-questionnaire): enhance type safety for questions in auto-answer

---------

Co-authored-by: Tofik Hasanov <annexcies@gmail.com>
@comp-ai-code-review
Copy link

comp-ai-code-review bot commented Nov 25, 2025

Comp AI - Code Vulnerability Scan

Analysis in progress...

Reviewing 30 file(s). This may take a few moments.


Powered by Comp AI - AI that handles compliance for you | Reviewed Nov 25, 2025, 07:01 PM

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@vercel
Copy link

vercel bot commented Nov 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
app (staging) Ready Ready Preview Comment Nov 25, 2025 7:27pm
portal (staging) Ready Ready Preview Comment Nov 25, 2025 7:27pm

* feat(email): add granular email unsubscribe preferences

- Add emailPreferences JSON field to User model for granular control
- Create unsubscribe preferences page with checkboxes for each email type
- Add unsubscribe API routes (GET/POST) with secure token verification
- Update all notification email templates to include unsubscribe links
- Add unsubscribe checks to email sending functions
- Create user settings page to re-subscribe from within app
- Support per-email-type unsubscribe (policy, task reminders, weekly digest, unassigned items)
- Use NEXT_PUBLIC_BETTER_AUTH_URL for unsubscribe links to support localhost/staging

* refactor(unsubscribe): remove legacy unsubscribe API and integrate preferences handling

* feat(user-settings): add user settings page for email notification preferences

* chore(auth): add default email preferences to mock user

---------

Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
@comp-ai-code-review
Copy link

comp-ai-code-review bot commented Nov 25, 2025

Comp AI - Code Vulnerability Scan

Analysis in progress...

Reviewing 30 file(s). This may take a few moments.


Powered by Comp AI - AI that handles compliance for you | Reviewed Nov 25, 2025, 07:24 PM

* feat(docs): add Trust Access documentation and update navigation

* refactor(docs): streamline Trust Access documentation for clarity

---------

Co-authored-by: Daniel Fu <itsnotaka@gmail.com>
Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
@comp-ai-code-review
Copy link

comp-ai-code-review bot commented Nov 25, 2025

🔒 Comp AI - Security Review

🔴 Risk Level: HIGH

OSV scan found 2 HIGH xlsx CVEs and 1 LOW ai CVE; code contains a hardcoded fallback secret (apps/app/src/lib/unsubscribe.ts) and multiple email/DB paths that use unvalidated email input.


📦 Dependency Vulnerabilities

🟠 NPM Packages (HIGH)

Risk Score: 8/10 | Summary: 2 high, 1 low CVEs found

Package Version CVE Severity CVSS Summary Fixed In
xlsx 0.18.5 GHSA-4r6h-8v6p-xvw6 HIGH N/A Prototype Pollution in sheetJS No fix yet
xlsx 0.18.5 GHSA-5pgg-2g8v-p4x9 HIGH N/A SheetJS Regular Expression Denial of Service (ReDoS) No fix yet
ai 5.0.0 GHSA-rwvc-j5jr-mgvh LOW N/A Vercel’s AI SDK's filetype whitelists can be bypassed when uploading files 5.0.52

🛡️ Code Security Analysis

View 18 file(s) with issues

🟡 apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts (MEDIUM Risk)

# Issue Risk Level
1 db.member.update uses id-only where; missing organization constraint MEDIUM
2 memberId only validated as string; no UUID/format verification MEDIUM
3 Role checks use string.includes(), allowing partial-match bypass MEDIUM
4 Potential TOCTOU: fetched targetMember not re-validated in update MEDIUM
5 console.error logs raw error details, may expose secrets in logs MEDIUM

Recommendations:

  1. Include organizationId in the db.member.update where clause (e.g., where: { id: memberId, organizationId: ctx.session.activeOrganizationId }) to ensure scoping and avoid accidental cross-organization changes.
  2. Tighten input validation for memberId (e.g., z.string().uuid() or a regex/length check) to ensure format and prevent malformed/overlong values being processed.
  3. Avoid using string.includes() for role checks. Use strict equality or an enum/array membership check (e.g., role === 'admin' || role === 'owner' or store role as enum and compare exactly) to prevent partial-match bypasses.
  4. Perform sensitive reads/updates inside a single DB transaction (or use a conditional update) to avoid TOCTOU issues. For example, UPDATE ... WHERE id = memberId AND organizationId = orgId AND deactivated = false and check affected rows.
  5. Avoid logging raw error objects in production. Redact or sanitize error details before logging, or log an internal error id and store full details in a secure error store accessible only to authorized personnel.
  6. Consider scoping session deletions and other side-effects to the organization context where appropriate (e.g., only delete sessions associated with access to this organization) to avoid broader impact.
  7. Add unit/integration tests for role strings and race conditions to validate intended behavior under edge cases.

🟡 apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/vendor-questionnaire-orchestrator.ts (MEDIUM Risk)

# Issue Risk Level
1 Untrusted header used for revalidatePath -> arbitrary path revalidation MEDIUM
2 Unbounded Promise.all for questions enabling DoS/resource exhaustion MEDIUM
3 Organization IDs logged in plaintext without redaction MEDIUM

Recommendations:

  1. Validate and whitelist the path before calling revalidatePath. Do not pass raw header values. Ensure the path is a relative internal path that starts with '/' and does not contain a protocol, host, or double-slash (//). Optionally parse with the WHATWG URL API and reject absolute URLs. Maintain a whitelist of allowed path prefixes (e.g., /^/orgs/[^/]+/security-questionnaire/) or map incoming referer headers to known internal routes.
  2. Sanitize header inputs: strip any protocol/host, remove query strings if not required, and reject or normalize suspicious inputs (e.g., containing 'http://' or 'https://' or starting with '//'). Explicitly reject external URLs.
  3. Limit concurrency when calling answerQuestion. Replace unbounded Promise.all with a concurrency control (e.g., p-limit) or process in small batches. Also consider Promise.allSettled to handle per-item failures and avoid aborting all work when one task fails. Example: const limit = pLimit(5); await Promise.all(questionsToAnswer.map(q => limit(() => answerQuestion(...))));
  4. Add rate-limiting and quotas at the API/auth layer for automated/large requests (per-user and per-organization) to mitigate resource exhaustion risks. Consider job-queuing large workloads rather than attempting them all in a single request.
  5. Redact or avoid logging sensitive organization identifiers. Instead of logging raw organizationId, log a hashed or truncated representation (e.g., orgId.slice(-4) or HMAC(orgId, serverSecret)) or mark logs as sensitive and restrict access. Review logging policy to ensure compliance with data protection requirements.
  6. Improve error handling around answer generation: catch and log individual question failures without failing the entire orchestration, and report partial success to the caller when appropriate.
  7. Perform per-organization authorization checks and enforce per-org quotas for long-running or resource-intensive operations. Ensure session.activeOrganizationId is sufficient for scope, and verify the session user is allowed to trigger revalidation for the computed path if that mapping exists.

🔴 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireAutoAnswer.ts (HIGH Risk)

# Issue Risk Level
1 No validation for questionsAndAnswers sent to server HIGH
2 credentials:'include' used without CSRF protection HIGH
3 Untrusted SSE JSON applied to app state without validation HIGH
4 Trusting server-provided questionIndex may overwrite wrong entries HIGH
5 Potential XSS if server-sent question/answer rendered unsanitized HIGH

Recommendations:

  1. Enforce server-side validation and authorization for the /api/security-questionnaire/auto-answer endpoint (validate structure, types, bounds for questionsAndAnswers and each element). Never rely solely on client-side validation.
  2. Protect state-changing endpoints from CSRF: require a same-site or httponly CSRF token (e.g., double-submit cookie or origin/Referer checks) if using credentials: 'include'. Alternatively, set cookies to SameSite=strict/lax where feasible and use explicit auth headers (bearer tokens) for API calls.
  3. Validate all SSE payload fields before using them: check data.type against an allowlist, ensure data.questionIndex is an integer within expected bounds, verify arrays (data.answers, data.sources) and their element shapes/lengths, and enforce reasonable maximum sizes to avoid resource exhaustion.
  4. Avoid blindly trusting server-provided indices. Map SSE messages to client state using a robust identifier (e.g., stable question IDs) rather than positional indices. If indices are used, validate they correspond to the expected question ID or are within array bounds before mutating state.
  5. Sanitize or escape question/answer strings before rendering in the UI. Treat SSE-provided strings as untrusted input. Use a safe rendering method (e.g., displaying as plain text, using a sanitizer library if HTML is required) to prevent XSS.
  6. Perform server-side checks that ensure the questions/answers being modified belong to the authenticated user/organization. Verify ownership and permissions on every mutation (e.g., in saveAnswersBatchAction).
  7. Harden the SSE handling: validate response Content-Type, limit message length, use a strict JSON parsing strategy with try/catch (already present) and fail-safe behavior, and consider authenticating SSE messages (signed payloads or token per-stream) if adversarial input is a concern.
  8. Log and monitor unexpected SSE messages or malformed payloads, and fail closed (stop processing) if a message deviates from the protocol.

🟡 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail/useQuestionnaireDetailHandlers.ts (MEDIUM Risk)

# Issue Risk Level
1 Unsanitized questions/answers forwarded to backend (auto/single) MEDIUM
2 Potential SQL injection via raw question/answer payloads MEDIUM
3 Potential command injection via raw question/answer payloads MEDIUM
4 No client-side validation for delete/update payloads MEDIUM
5 deleteAnswerAction.execute invoked without explicit auth/ownership check MEDIUM
6 Answer queue indices used/processed without validation MEDIUM

Recommendations:

  1. Treat all question/answer text as untrusted. Validate and sanitize on the server-side before any DB or OS/exec usage. Apply escaping and length/content constraints.
  2. On the server use parameterized/ prepared statements (no string concatenation) for any DB queries that use question/answer payloads.
  3. Do not pass raw user content into any command execution, template evaluation, or eval-style APIs. If any backend consumes these values for commands, remove that pattern or strictly validate/escape.
  4. Ensure server-side authorization checks for update/delete actions (verify user/org ownership and permissions) — do not rely on client-side checks.
  5. Add strong input validation on the client (type checks, index bounds, deduping) and on the server (type, bounds, and semantic validation). Specifically validate queue indices are integers within expected range before processing.
  6. Add error handling and defensive checks around action.execute calls; treat failures gracefully and do not assume backend performed all validations.
  7. Log minimal sensitive data and avoid echoing raw inputs back into UI without escaping/encoding to mitigate XSS.

🟡 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail/useQuestionnaireDetailState.ts (MEDIUM Risk)

# Issue Risk Level
1 Delete action callable without client-side confirmation MEDIUM
2 Tokens stored in client state may expose secrets MEDIUM
3 Console.error may leak sensitive error details MEDIUM

Recommendations:

  1. Add an explicit client-side confirmation (modal/undo) before invoking delete to prevent accidental deletions; keep server-side authorization/ownership checks (already present).
  2. Avoid persisting sensitive tokens in plain client-side state. If tokens must be client-consumable, prefer short-lived public tokens generated server-side (as this project already does for trigger tokens) and avoid storing long-lived secrets in React state or localStorage. Remove unused token state variables if no longer needed.
  3. Remove or reduce console.error output in production for sensitive errors. On the client, avoid logging whole error objects returned from server actions; show user-friendly messages and log structured errors server-side (with proper access controls).
  4. Ensure output/rendering of stored answers is properly escaped or sanitized when rendered in the UI to prevent XSS — even though server validates input with zod, treat DB-stored strings as untrusted when rendering.
  5. Keep or add server-side validation/authorization for all actions (already implemented for update/delete via zod schemas and organization/user checks).

🟡 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireState.ts (MEDIUM Risk)

# Issue Risk Level
1 orgId from URL used without validation MEDIUM
2 Uploaded File stored without validation or size/type checks MEDIUM
3 extractedContent and searchQuery not sanitized before use MEDIUM
4 Sensitive tokens stored in client state (parse/auto/single tokens) MEDIUM
5 resetState does not clear parse/auto/single tokens MEDIUM

Recommendations:

  1. Validate and whitelist orgId: apply strict validation (e.g., UUID or expected slug regex) as soon as you read params and reject or normalize unexpected values. Fail closed and/or handle missing/invalid orgId server-side.
  2. Add file validation on upload (client and server): enforce allowed MIME types and extensions, max size limits, and run server-side virus/malware scanning. Block or sanitize unsupported file contents. Do not rely solely on client-side checks.
  3. Sanitize/encode extractedContent and searchQuery before use or rendering: treat any file-extracted or user-entered text as untrusted. Use output encoding/escaping for the target context (HTML escaping or a library like DOMPurify before using innerHTML). Also apply input validation where appropriate.
  4. Avoid storing sensitive tokens in accessible client JS state: do not persist long-lived sensitive tokens in React state/localStorage. Prefer HttpOnly, Secure cookies or server-side sessions; if a token must exist client-side, minimize lifetime and scope and store only in memory with strong XSS protections.
  5. Ensure resetState clears all sensitive data: include setParseToken(null), setAutoAnswerToken(null), setSingleAnswerToken(null), setParseTaskId(null) and clear answeringQuestionIndices/hasClickedAutoAnswer as needed so tokens and task identifiers are not left in memory after reset/logout.
  6. Defensive measures: implement strong Content Security Policy (CSP), input validation on all server endpoints, HTTPS everywhere, and regular dependency/security scans to reduce impact of client-side stored secrets if XSS occurs.

🟢 apps/app/src/app/(app)/[orgId]/settings/user/actions/update-email-preferences.ts (LOW Risk)

# Issue Risk Level
1 revalidatePath uses session.activeOrganizationId without sanitization LOW
2 Zod schema not strict — extra preference keys allowed LOW
3 Console.error may log sensitive data LOW
4 DB update uses email as identifier (potential collisions) LOW

Recommendations:

  1. Validate/sanitize ctx.session.activeOrganizationId before using it in revalidatePath: ensure it's the expected format (e.g., UUID or integer), belongs to the user's organizations, and/or restrict to an allowlist of org IDs. Reject or normalize unexpected values.
  2. Make the Zod schema strict to prevent unexpected keys from being accepted (e.g., z.object({...}).strict()), or explicitly .strict() / .passthrough() depending on desired behavior. Also validate that preference keys are exactly the expected set.
  3. Avoid logging raw error objects to console. Log sanitized messages and non-sensitive error codes only; send full errors to a secure error-tracking system (Sentry) with access controls. Consider scrubbing PII before logging.
  4. Use a stable, immutable user identifier for DB updates (e.g., user.id) instead of email. Ensure the DB has a unique constraint on email if email is used elsewhere. Also confirm the authenticated user is authorized to modify the target record.

🟢 apps/app/src/app/(app)/[orgId]/settings/user/page.tsx (LOW Risk)

# Issue Risk Level
1 No validation of session.user.email before DB query LOW
2 Unsafe object spread of emailPreferences may allow prototype pollution LOW
3 No server-side validation of emailPreferences value types LOW

Recommendations:

  1. Normalize and validate session.user.email before using it in DB queries: ensure it's a string, matches an email regex (or use a dedicated validator), and enforce length limits. Also handle and log failures from auth.api.getSession and avoid trusting malformed session data.
  2. Whitelist allowable preference keys and perform an explicit merge instead of spreading the whole saved object. Example approach: const allowed = ['policyNotifications','taskReminders','weeklyTaskDigest','unassignedItemsNotifications']; const sanitized = {}; for (const k of allowed) { sanitized[k] = Boolean((user.emailPreferences as Record<string, any>)?.[k]); } const preferences = { ...DEFAULT_PREFERENCES, ...sanitized };
  3. Coerce and validate each preference value server-side to boolean (e.g., Boolean(value) or value === true) and reject/ignore unexpected keys or non-boolean types. Use a schema validator (zod/Joi/Yup) on the emailPreferences object retrieved from the DB.
  4. Prevent prototype-pollution by never copying unknown keys directly onto plain objects. Use a fresh object with only whitelisted keys (as above) or build the result with Object.create(null) and assign only allowed keys. Avoid blind object spread of data coming from persistence.
  5. Add type/schema constraints at the persistence layer where possible (e.g., enforce JSON schema or DB constraints for emailPreferences) so invalid shapes do not get stored.
  6. Add logging/monitoring for unexpected shapes/types in session.user and user.emailPreferences so anomalous data can be detected and remediated.

🟡 apps/app/src/app/api/security-questionnaire/answer-single/route.ts (MEDIUM Risk)

# Issue Risk Level
1 Unvalidated path from headers used in revalidatePath MEDIUM
2 User input (question) passed to answerQuestion without sanitization MEDIUM
3 Detailed error messages and zod details returned to clients MEDIUM
4 Potential sensitive data logged (raw error.message) MEDIUM
5 No CSRF protection on POST; cookie session could be abused MEDIUM
6 No rate limiting; endpoint can be abused to trigger expensive tasks MEDIUM

Recommendations:

  1. Validate and whitelist revalidation paths before calling revalidatePath. Only allow a predefined set of safe paths or normalize + verify the path is inside the app (e.g., startsWith('/app') and not containing '../'). Do not accept arbitrary 'referer' or 'x-pathname' values from headers.
  2. Tighten input validation for 'question' using zod (e.g., .min(1).max(200).regex(...) as appropriate) and sanitize/escape content before passing to downstream functions. If answerQuestion expects structured input, enforce stricter types and limits in the schema.
  3. Do not return raw validation errors or internal error messages to clients. Return a generic error message (e.g., 'Invalid input') and log details server-side for debugging only. Remove or redact zod error details from public responses.
  4. Avoid logging raw error.message or untrusted input directly. Redact or mask potentially sensitive fields, and log structured context (error id, organization id, but avoid full stack/PII in logs). Consider generating an error ID to surface to the client and correlate with logs.
  5. Implement CSRF protections: require and verify an anti-CSRF token (double-submit cookie or SameSite=strict cookies + custom header verification) for state-changing POST endpoints. Alternatively, ensure that session cookies are not susceptible to cross-site requests and require an explicit auth header if appropriate.
  6. Add rate limiting and throttling per IP and per organization to prevent abuse and expensive task flooding. For expensive operations, offload to a background job queue and return an acknowledgement immediately instead of awaiting heavy work synchronously.
  7. When revalidating, ensure path normalization (strip query strings, fragments) and enforce a strict whitelist or mapping from referer/header to allowed revalidate paths to prevent unintended cache revalidation.
  8. If answerQuestion performs external requests or invokes other subsystems, ensure it also performs its own input validation/sanitization and has resource/time limits to avoid injection and DoS risks.

🔴 apps/app/src/app/api/security-questionnaire/auto-answer/route.ts (HIGH Risk)

# Issue Risk Level
1 No input validation for questionsAndAnswers HIGH
2 Unbounded parallel processing may cause DoS/resource exhaustion HIGH
3 No request size limits or rate limiting HIGH
4 Possible sensitive data leakage via SSE (error.message and sources) HIGH
5 No CSRF protection for cookie-based session requests HIGH
6 Insufficient authorization checks beyond activeOrganizationId HIGH

Recommendations:

  1. Validate and strictly type the request body (questionsAndAnswers). Enforce per-item schema (question: string, answer: string|null, optional _originalIndex:number) and set a reasonable maximum number of questions (e.g., max 50). Return 4xx for invalid input.
  2. Limit concurrency when processing questions (e.g., p-limit, queue with worker pool) instead of firing all promises at once. Consider batching and back-pressure to avoid resource exhaustion.
  3. Enforce request size limits and apply global/route rate limiting (e.g., API gateway, middleware) and body-parser limits to prevent large payloads or high request rates.
  4. Sanitize or omit internal error messages and sensitive fields from SSE output. Do not stream raw error.message or internal 'sources' metadata to clients; replace with generic error codes/messages and require explicit permission to return source metadata.
  5. Protect cookie-based endpoints against CSRF: require SameSite=strict cookies, validate Origin/Referer, and/or require a CSRF token in POST requests (or use double-submit cookie pattern).
  6. Perform stronger authorization checks: verify the session user is authorized for the activeOrganizationId and check roles/permissions for performing auto-answer operations. Log and reject requests where the user lacks required privileges.

🟡 apps/app/src/app/unsubscribe/page.tsx (MEDIUM Risk)

# Issue Risk Level
1 Unvalidated email param used in DB query MEDIUM
2 User enumeration via differing page responses revealing account status MEDIUM
3 Unauthenticated unsubscribe flow possible via email param-generated link MEDIUM
4 Exposes user email (PII) directly in page output MEDIUM

Recommendations:

  1. Validate and canonicalize the email search param server-side (strict email regex, max length) before using it in any logic or DB query.
  2. Avoid revealing account existence in responses: return a consistent, generic page for both existing and non-existing emails (do not vary content based on db.user.findUnique result).
  3. Protect the unsubscribe action: require a one-time signed token that cannot be trivially obtained by simply requesting the unsubscribe page (e.g., include a token delivered only in email or require a POST with CSRF protection). Do not generate actionable tokens solely in response to an arbitrary GET with an email param.
  4. Do not render raw PII directly from query params. Mask the email (e.g., j***@d****.com) or require confirmation via a protected flow so the value shown is derived from a verified source.
  5. Rate-limit and log unsubscribe attempts (by IP and target email) to detect abuse and brute-force probing of addresses.
  6. Ensure all database access uses the ORM's parameterized APIs (Prisma already does this) and avoid building raw SQL from unchecked query params.

🟡 apps/app/src/app/unsubscribe/preferences/actions/update-preferences.ts (MEDIUM Risk)

# Issue Risk Level
1 Distinct error messages allow user enumeration by email MEDIUM
2 No rate limiting on unsubscribe action enables token brute-force MEDIUM
3 Console.error logs may expose sensitive DB error details MEDIUM
4 Token validation may lack expiry or revocation checks MEDIUM

Recommendations:

  1. Return uniform error responses for failures (e.g., always return a generic "Failed to update preferences"), and surface minimal status to callers to avoid allowing an attacker to infer account existence or token validity.
  2. Implement rate limiting / throttling on this server action (per IP, per email, per token) and consider exponential backoff or temporary blocking for repeated failures to mitigate brute-force token attacks.
  3. Avoid logging raw error objects to console in production. Sanitize errors before logging and use structured logging with redaction. Capture full stack/details in a secure error-tracking system (accessible only to ops) if needed.
  4. Ensure verifyUnsubscribeToken enforces token expiry, single-use/revocation where appropriate, and binds tokens to the intended email/address. If verifyUnsubscribeToken is a blackbox here, add server-side checks for issued/expired/revoked tokens in the DB.
  5. If you intend to reject unknown preference fields, make the Zod schema explicit (e.g., use .strict() to throw on unknown keys). If you prefer silently stripping unknown fields, documenting reliance on Zod's default behavior is recommended.

🔴 apps/app/src/app/unsubscribe/preferences/client.tsx (HIGH Risk)

# Issue Risk Level
1 Token prop (sensitive) is exposed to client-side code HIGH
2 Token may leak via Referer or third-party scripts HIGH
3 Missing validation of email, token, and preferences before submit HIGH
4 User-visible serverError shown directly (information leak) HIGH

Recommendations:

  1. Do not pass long-lived or sensitive tokens into client-side React components. Handle token verification server-side (e.g., route that accepts a short-lived one-time token or a server-side session) and render only non-sensitive state to the client.
  2. Use short, single-use unsubscribe tokens that expire quickly. Treat unsubscribe tokens as bearer secrets: avoid placing them in URLs or storing them in persistent client storage.
  3. Avoid including tokens in query strings that can appear in Referer headers. Configure Referrer-Policy (e.g., no-referrer or same-origin) and use a strict Content Security Policy. Prefer POST requests from server-rendered pages or server-side handlers so tokens are not exposed in URLs.
  4. Do not render raw server error messages to the user. Replace serverError with a generic user-facing message and log detailed errors server-side (with correlation IDs if needed).
  5. Validate and sanitize email, token, and preferences on the server before applying changes. Add client-side validation to improve UX but do not rely on it for security.
  6. If you must expose any token-like value to the client, use a masked or scoped token (one-time or limited-scope), and rotate/expire it quickly.
  7. Restrict third-party scripts on pages that handle sensitive tokens. If third-party scripts are required, audit them and use subresource integrity and strict CSP to limit risk of exfiltration.
  8. Use secure cookies (HttpOnly, Secure, SameSite) for authenticated flows where possible instead of exposing tokens to client JS.

🟡 apps/app/src/app/unsubscribe/preferences/page.tsx (MEDIUM Risk)

# Issue Risk Level
1 Query params used directly in DB query (email input used without validation) MEDIUM
2 Missing validation of email and token inputs MEDIUM
3 Distinct error messages reveal whether an email exists (user enumeration) MEDIUM
4 Token is taken from URL and passed to client (exposed in browser/logs/referrers) MEDIUM
5 No rate limiting on token verification (brute-force token guessing) MEDIUM
6 User emailPreferences merged without validation (data integrity/prototype risk) MEDIUM

Recommendations:

  1. Validate and sanitize email and token before use: enforce email format (RFC 5322 or a sensible subset), enforce token length/charset, and reject invalid values early.
  2. Make verifyUnsubscribeToken authoritative for both token validity and input sanity. If it currently only checks the token cryptographically, add explicit validation of the email format and token shape there or before calling the DB.
  3. Avoid leaking existence via distinct error messages. Return a single ambiguous error message for any failure (e.g., "Invalid request or link") or always return the same UX and only log details server-side.
  4. Do not expose long-lived sensitive tokens in URLs or client props. Use a one-time, short-lived server-side exchange: user clicks URL with token, server validates and issues a short-lived session cookie or single-use ephemeral code, and only the ephemeral code is used by the client if needed.
  5. Add rate limiting and throttling on token verification and related endpoints (per-IP and per-account), and implement exponential backoff or temporary bans for repeated failures. Consider logging and alerting suspicious activity.
  6. Whitelist and validate emailPreferences before merging: accept only known keys and boolean values. Use a schema validator (e.g., zod/joi) to coerce/validate the shape and types. Avoid blindly spreading user-provided objects into defaults (prevents prototype pollution and unexpected keys).
  7. Ensure DB access uses parameterized queries/ORM properly (this code uses an ORM but verify the ORM layer is not constructing raw SQL from user input).
  8. If possible, consume unsubscribe tokens on first use (one-time tokens) to reduce replay risk, and rotate/expire tokens quickly.

🟡 apps/app/src/jobs/tasks/email/new-policy-email.ts (MEDIUM Risk)

# Issue Risk Level
1 No validation of payload fields (email/name/policyName) MEDIUM
2 Potential SQL injection via isUserUnsubscribed using raw email input MEDIUM
3 Unvalidated email content in sendPolicyNotificationEmail (template injection/XSS) MEDIUM
4 Logs contain PII (email, policyName) without redaction MEDIUM
5 Concurrency limit misconfigured; may allow excessive sends MEDIUM

Recommendations:

  1. Validate and sanitize all fields in PolicyEmailPayload at task entry: enforce email format (RFC 5322/basic regex), max lengths, and disallow unexpected control characters.
  2. Inspect and harden isUserUnsubscribed implementation: ensure it uses parameterized queries/ORM APIs and does not interpolate user input into raw SQL. Add unit tests for malicious inputs.
  3. Inspect and harden sendPolicyNotificationEmail: escape or sanitize template variables before rendering (or use a safe templating engine that auto-escapes). Treat any user- or org-controlled values as untrusted input.
  4. Avoid logging raw PII. Mask or omit sensitive fields (e.g., log hashed or partially redacted email like j***@example.com). Use structured logging with a privacy filter that strips or redacts PII before emission.
  5. Reconcile concurrencyLimit with intended rate (comment says 1/sec but concurrencyLimit is 2). Implement explicit rate limiting/throttling (token bucket or centralized rate limiter) if strict per-second limits are required.
  6. Add defensive error handling and monitoring: track email send failures, retries, and a DLQ for repeated failures. Add unit/integration tests that assert input validation and SQL/template sanitization behavior.

🟡 apps/app/src/jobs/tasks/email/publish-all-policies-email.ts (MEDIUM Risk)

# Issue Risk Level
1 No input validation for payload fields (email, userName, organizationId) MEDIUM
2 Potential SQL injection via isUserUnsubscribed(db, payload.email) MEDIUM
3 No authorization check before sending emails (any task caller can enqueue) MEDIUM
4 Sensitive PII logged (email) without redaction MEDIUM
5 No email format/header validation; risk of email header injection MEDIUM

Recommendations:

  1. Validate and sanitize the payload at task entry: enforce a strict schema (email format, max lengths, allowed chars for userName and organizationName, UUID format for organizationId) and reject/normalize invalid inputs before any DB or email logic.
  2. Review and harden isUserUnsubscribed implementation: ensure it uses parameterized queries / prepared statements or ORM parameter binding; never build SQL with string concatenation using payload values. Add unit tests demonstrating safe handling of malicious email strings.
  3. Add authorization/ACL checks around task enqueueing and/or inside the task runner: ensure only authorized services/users can request sending these emails and that organizationId in the payload is owned by the caller or validated against an auth token.
  4. Avoid logging full PII. Redact or hash emails in logs (e.g., replace localpart or show only hashed value) and ensure logger config/classification prevents sensitive data from being exported to low-security sinks.
  5. Validate and sanitize email content and headers before sending: enforce strict email address validation, and ensure the email-sending library/template escapes header and body fields to prevent header injection. Add tests for header injection attempts.
  6. Add observability and safe error handling: capture non-PII contextual info for debugging, consider structured telemetry with PII filters, and implement retry/backoff for transient failures while ensuring errors don't leak sensitive data.
  7. If sendAllPolicyNotificationEmail is in a separate module, perform a security review of that function (template rendering, header construction, third-party provider usage) and ensure it also validates/escapes inputs.

🟡 apps/app/src/jobs/tasks/email/weekly-task-digest-email.ts (MEDIUM Risk)

# Issue Risk Level
1 Possible SQL injection via isUserUnsubscribed(db, payload.email) MEDIUM
2 No validation/sanitization of payload fields (email, ids, titles) MEDIUM
3 Potential email header or content injection via sendWeeklyTaskDigestEmail MEDIUM
4 Sensitive PII (email) logged in plaintext MEDIUM

Recommendations:

  1. Add explicit input validation and normalization for the task payload. Use a schema validator (zod, Joi, or similar) to validate email, id formats, title lengths/characters, and the shape of payload.tasks before any DB or email usage.
  2. Review the implementation of isUserUnsubscribed(db, email, ...) and ensure it uses parameterized/ prepared statements or an ORM query builder. Do not build SQL strings using raw/concatenated user input. If using raw queries, bind email as a parameter.
  3. Sanitize and escape any user-controlled content used in email headers or body. Validate header fields and use templating with automatic escaping for body content. Strip CRLF from header values (subject, from, to) to prevent email header injection.
  4. Avoid logging raw PII. Mask or hash email addresses before logging (e.g., j***@example.com or log a stable hash) or remove email from non-essential logs. Use structured logging with redaction for sensitive fields in production.
  5. Add end-to-end checks: instrument tests that assert the DB helper functions do not accept raw SQL fragments and that email-sending code rejects or escapes suspicious input (CR/LF sequences).
  6. If payloads are enqueued from external sources (API requests), validate at the API boundary and sanitize again before queuing. Treat queued payloads as potentially untrusted.

🔴 apps/app/src/lib/unsubscribe.ts (HIGH Risk)

# Issue Risk Level
1 Hardcoded fallback secret 'fallback-secret' in code HIGH
2 Secret may be reused from AUTH_SECRET/UNSUBSCRIBE_SECRET HIGH
3 Unbounded token lifetime: tokens have no expiry or rotation HIGH
4 Token appears in URL query, risking logs/referrers leakage HIGH
5 Insecure token comparison using === (timing attack possible) HIGH
6 No input validation for email or token before use HIGH

Recommendations:

  1. Remove the hardcoded fallback secret. Require a dedicated environment variable (e.g., NEXT_PUBLIC_UNSUBSCRIBE_SECRET or UNSUBSCRIBE_SECRET) and fail fast on startup if not set.
  2. Do not reuse AUTH_SECRET for unsubscribe tokens. Use a separate, strong secret specifically for unsubscribe/verification tokens to limit blast radius.
  3. Add token expiry/rotation. Include a timestamp in the HMAC input (or sign a short-lived JWT) and validate the timestamp on verify to ensure tokens become invalid after a short window.
  4. Avoid placing long-lived secrets/tokens in URL query parameters. Use POST endpoints, one-time tokens stored server-side, short-lived tokens, or a path-based one-time identifier to reduce logging/referrer leakage.
  5. Use a constant-time comparison for token verification. For example, compare Buffer.from(expected) and Buffer.from(actual) with crypto.timingSafeEqual, handling unequal lengths safely (first check lengths).
  6. Validate inputs: enforce a strict email format and acceptable token character sets/lengths before use. Sanitize/encode before embedding in URLs (you already use encodeURIComponent for the URL, continue to do so).
  7. Harden deployment practices: store secrets in a secrets manager, rotate secrets periodically, and log only non-sensitive operational data. Consider signing tokens with an algorithm that includes expiry (e.g., signed JWTs) if appropriate for your use case.

💡 Recommendations

View 3 recommendation(s)
  1. Upgrade/patch the vulnerable npm packages: update xlsx (remediate GHSA-4r6h-8v6p-xvw6 and GHSA-5pgg-2g8v-p4x9) and update ai to >= 5.0.52. Re-run the audit and lock to patched versions in package.json.
  2. Remove the hardcoded fallback secret in apps/app/src/lib/unsubscribe.ts. Read the unsubscribe secret from a required environment variable and fail startup if it is missing; avoid embedding any default secret in source.
  3. Fix injection vectors around email usage: validate and canonicalize email inputs before DB/SMTP use (apply strict regex/length checks), ensure isUserUnsubscribed and all DB calls use parameterized ORM queries (no string interpolation), and strip/encode CRLF from header fields before sending emails.

Powered by Comp AI - AI that handles compliance for you. Reviewed Nov 25, 2025

@claudfuen
Copy link
Contributor

🎉 This PR is included in version 1.64.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants