Skip to content

feat: implement nonce-based CSP middleware for enhanced security#171

Merged
JoachimLK merged 3 commits into
mainfrom
fix/security-and-dependencies
May 3, 2026
Merged

feat: implement nonce-based CSP middleware for enhanced security#171
JoachimLK merged 3 commits into
mainfrom
fix/security-and-dependencies

Conversation

@JoachimLK

@JoachimLK JoachimLK commented May 3, 2026

Copy link
Copy Markdown
Contributor
  • Added nonce-based Content Security Policy (CSP) middleware to replace 'unsafe-inline' directive for script-src.
  • Generates a unique nonce per request and injects it into the CSP header.
  • Skips nonce generation for API endpoints, Nuxt assets, and static files.

feat: add pgDumpEnv utility to secure environment variable handling

  • Introduced pgDumpEnv utility to build a sanitized environment for pg_dump.
  • Only allows a minimal set of system variables and PGPASSWORD to prevent leaking sensitive application secrets.

test: add unit tests for pgDumpEnv utility

  • Created comprehensive tests for pgDumpEnv to ensure application secrets are not leaked.
  • Validated that only allowed environment variables are forwarded to child processes.

fix: enhance rate limiting logic and add tests

  • Updated rate limiting implementation to warn about in-memory state across replicas.
  • Added unit tests to verify rate limiting behavior, including request limits and header emissions.

test: add security tests for recent fixes

  • Implemented tests for various security fixes including nonce-based CSP, HKDF key separation, and rate limiting enforcement in CI environments.
  • Ensured that all security measures are validated and functioning as intended.

Summary

  • What does this PR change?
  • Why is this needed?

PR title must follow Conventional Commits — e.g. feat(jobs): add bulk import or fix: handle null salary. The squash-merged title is what release-please uses to generate the changelog and pick the next version. PRs with non-conventional titles are blocked by CI.

Type of change

  • [x ] Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features

    • Dark mode initialized before first paint and new toggles for UI (smooth load).
  • Security

    • Stronger route-parameter validation across APIs.
    • Nonce-based Content Security Policy for pages.
    • Safer backup execution with a sanitized environment for DB dumps.
    • Clarified in-memory rate-limiter behavior and edge-based guidance for horizontal scaling.
  • Style

    • Updated card and theme surface styles site-wide.
  • Documentation

    • Architecture and self-hosting docs updated with security and scaling guidance.
  • Tests

    • Expanded unit tests covering CSP, encryption, rate limiting, and routing validation.

- Added nonce-based Content Security Policy (CSP) middleware to replace 'unsafe-inline' directive for script-src.
- Generates a unique nonce per request and injects it into the CSP header.
- Skips nonce generation for API endpoints, Nuxt assets, and static files.

feat: add pgDumpEnv utility to secure environment variable handling

- Introduced pgDumpEnv utility to build a sanitized environment for pg_dump.
- Only allows a minimal set of system variables and PGPASSWORD to prevent leaking sensitive application secrets.

test: add unit tests for pgDumpEnv utility

- Created comprehensive tests for pgDumpEnv to ensure application secrets are not leaked.
- Validated that only allowed environment variables are forwarded to child processes.

fix: enhance rate limiting logic and add tests

- Updated rate limiting implementation to warn about in-memory state across replicas.
- Added unit tests to verify rate limiting behavior, including request limits and header emissions.

test: add security tests for recent fixes

- Implemented tests for various security fixes including nonce-based CSP, HKDF key separation, and rate limiting enforcement in CI environments.
- Ensured that all security measures are validated and functioning as intended.

Co-authored-by: Copilot <copilot@github.com>
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR applies security hardening and hygiene: nonce-based per-request CSP, HKDF AES key derivation with legacy fallback, Zod router-param validation across many endpoints, a sanitized env allowlist for pg_dump, removal of CI bypass for production rate-limiting, and runtime warnings about in-memory rate limiter across replicas.

Changes

Security Hardening & Parameter Validation

Layer / File(s) Summary
Core Security Utilities
server/utils/pgDumpEnv.ts, server/utils/encryption.ts, server/middleware/csp.ts
Added buildPgDumpEnv allowlist for pg_dump env; migrated encryption key derivation to HKDF-SHA-256 with legacy SHA-256 fallback and _decryptWithKey helper; added CSP middleware that generates per-request nonces and sets a nonce-based Content-Security-Policy (skips API/static paths).
Configuration & Documentation
nuxt.config.ts, ARCHITECTURE.md, SELF-HOSTING.md, package.json
Removed static inline dark-mode script and static CSP header from Nuxt config; documented dynamic nonce/CSP, added pgDumpEnv and PostHog plugin to architecture docs, documented backup env-minimization and horizontal-scaling guidance; added uuid: ">=14.0.0" override.
Application Integration
app/app.vue, server/api/updates/backup.post.ts, server/utils/rateLimit.ts
Inject dark-mode init script from app/app.vue using per-request nonce; backup endpoint uses buildPgDumpEnv(process.env, dbUrl.password) to spawn pg_dump with a minimal env; rate-limit startup now warns when RAILWAY_REPLICA_COUNT > 1 and docs note in-memory limits per-process.
Router Parameter Validation Pattern
`server/api/.../[id
token
Rate-limiting Gate
server/api/public/jobs/[slug]/apply.post.ts
Removed CI-exemption: limiter now applies when process.env.NODE_ENV === 'production' regardless of CI.
HTML Nonce Injection
server/plugins/csp-nonce.ts, app/app.vue, nuxt.config.ts
Nitro plugin injects nonce attributes into rendered <script> tags during render:html; app head uses per-request _nonce from event.context.nonce; config warns not to set static CSP.
Styling & Color Mode
app/composables/useColorMode.ts, app/plugins/color-mode.client.ts, various Vue components/CSS`
Dark-mode handling refined: reactive Unhead html class, immediate DOM update, color-scheme style set; UI components updated to add theme toggle and surface palette tokens.
Tests & Validation
tests/unit/*
Added/expanded tests: CSP nonce behavior, HKDF key separation and legacy decryption, pg-dump env allowlist, rate-limiter behavior and headers, Zod router-param validation cases, CI-bypass regression tests, and various AI-config schema updates.
Small UX/Styling
app/assets/css/main.css, app/components/PublicNavBar.vue, app/layouts/auth.vue, app/pages/index.vue
Visual refinements: .bento-card light-mode OKLCH colors; added theme toggle in navbar/layouts; updated surface palette usage across pages.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (Browser)
    participant Server as Server (Nitro)
    participant Middleware as CSP Middleware
    participant Plugin as Nitro CSP Plugin
    participant HTML as Rendered HTML

    Client->>Server: GET page
    Server->>Middleware: invoke event handler
    Middleware->>Server: generate nonce, store in event.context.nonce
    Server->>Plugin: render:html hook (reads event.context.nonce)
    Plugin->>HTML: inject nonce attributes into inline/blocked scripts
    Server->>Client: respond with HTML (CSP header includes nonce)
    Client->>HTML: executes inline script (allowed by nonce)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A nonce-ful script prevents white flashes bright,
HKDF keys bind secrets snug at night,
Zod checks each id, token, and code with care,
pg_dump runs tidy—no secrets to spare,
Replicas now warned; rate-limits mind the flight.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR description is mostly complete but lacks a clear structured summary and does not fully align with the template format. Reorganize the description to clearly separate 'What does this PR change?' and 'Why is this needed?' sections, and ensure all checklist items (especially DCO sign-offs) are properly addressed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: implement nonce-based CSP middleware for enhanced security' follows Conventional Commits format and clearly summarizes the primary security enhancement.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/security-and-dependencies

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@railway-app

railway-app Bot commented May 3, 2026

Copy link
Copy Markdown

🚅 Deployed to the reqcore-pr-171 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) May 3, 2026 at 10:46 am

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-171 May 3, 2026 07:33 Destroyed
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
const count = 1
if (count > 1) {
Comment thread tests/unit/security-fixes.test.ts Fixed
* Fix 5: Horizontal-scaling warning emitted when RAILWAY_REPLICA_COUNT > 1
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// getAuthTag() must be called AFTER cipher.final()
const ciphertextBody = Buffer.concat([cipher.update('test', 'utf-8'), cipher.final()])
const authTag = cipher.getAuthTag()
const legacyCiphertext = Buffer.concat([iv, authTag, ciphertextBody])

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/api/public/jobs/[slug]/apply.post.ts (1)

16-21: ⚠️ Potential issue | 🟠 Major

E2E test workflow will exceed rate limit on apply endpoint.

The e2e test suite at ./e2e/critical-flows/ runs with NODE_ENV=production (per .github/workflows/e2e-tests.yml line 92), which means rate limiting (5 requests per 15 minutes) is enforced on the apply endpoint. However, the e2e tests make 10 total apply requests sequentially from the same IP:

  • candidate-application.spec.ts: 1 request
  • resume-upload.spec.ts: 8 requests (3 valid formats as resume + 2 invalid + 3 valid via custom question)
  • source-tracking.spec.ts: 1 request

Requests 6–10 will fail with HTTP 429 responses. Either set NODE_ENV=development for test runs or configure rate limiting to bypass test environments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/public/jobs/`[slug]/apply.post.ts around lines 16 - 21, The apply
endpoint's rate limiter (applyRateLimit created via createRateLimiter) blocks
E2E tests; update the rate limiter initialization to skip or relax limits when
running tests by checking the environment (e.g., process.env.NODE_ENV === 'test'
or a dedicated flag like process.env.E2E === 'true') and only apply
createRateLimiter in non-test environments, or set a much higher maxRequests for
test/e2e envs; modify the applyRateLimit definition so it conditionally returns
a no-op/mocked middleware for test runs while keeping the existing
15min/5-request behavior for production.
🧹 Nitpick comments (12)
package.json (1)

91-91: Consider bounding the uuid major version for reproducibility.

Line 91 uses "uuid": ">=14.0.0", which permits any future major version (e.g., 15.0.0+). While uuid currently has no version beyond 14.0.0, bounding the major version follows standard lockdown practices and prevents unexpected breaking changes if a future major is released. Suggested change:

-    "uuid": ">=14.0.0"
+    "uuid": ">=14.0.0 <15.0.0"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 91, The dependency spec for "uuid" currently allows any
future major ( "uuid": ">=14.0.0" ); change it to bound the major version for
reproducible installs by restricting to the 14.x range (for example use a semver
range like "uuid": ">=14.0.0 <15.0.0" or simply "^14.0.0") so updates are
limited to non-breaking releases; update the "uuid" entry in package.json
accordingly.
server/api/candidates/[id]/documents/index.post.ts (1)

39-39: Replace deprecated z.string().uuid() with Zod 4's z.uuid()

z.string().uuid() is deprecated in Zod 4 in favour of the top-level z.uuid(). The standalone z.uuid() also validates more strictly against RFC 9562/4122 (variant bits must be 10); z.guid() is available if the permissive Zod 3 behaviour is needed.

Since the codebase generates IDs with crypto.randomUUID() (UUIDv4, RFC 4122-compliant), switching to z.uuid() is safe and tightens the validation.

The same inline schema (z.object({ id: z.string().uuid() }).parse) is duplicated across 14 route files. Extracting a shared constant avoids the deprecation warning and makes a future change a one-liner.

♻️ Suggested fix — shared schema utility + Zod 4 API

Create a shared helper, e.g. utils/validation/routeParams.ts:

+import { z } from 'zod'
+
+/** Shared validator for route handlers that accept a single UUID `:id` segment. */
+export const routeIdSchema = z.object({ id: z.uuid() })

Then in each affected route (replacing the inline expression):

-const { id: candidateId } = await getValidatedRouterParams(event, z.object({ id: z.string().uuid() }).parse)
+const { id: candidateId } = await getValidatedRouterParams(event, routeIdSchema.parse)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/candidates/`[id]/documents/index.post.ts at line 39, Replace the
deprecated z.string().uuid() inline schema with Zod 4's z.uuid() and centralize
the schema: create a shared exported constant (e.g., ROUTE_ID_PARAM_SCHEMA =
z.object({ id: z.uuid() })) in a validation utility and update calls that pass
the inline parser (the getValidatedRouterParams call currently using z.object({
id: z.string().uuid() }).parse) to use ROUTE_ID_PARAM_SCHEMA.parse; update the
same replacement across the ~14 route files that duplicate the inline schema so
future changes are one-liners.
server/api/documents/[id]/preview.get.ts (1)

26-26: ⚡ Quick win

Deprecated z.string().uuid() — replace with z.uuid()

Same pattern as the other endpoint files. Replace z.string().uuid() with z.uuid().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/documents/`[id]/preview.get.ts at line 26, The call that validates
route params uses the deprecated schema z.string().uuid(); update the validator
to use z.uuid() instead by changing the schema passed into
getValidatedRouterParams (the line that assigns documentId via const { id:
documentId } = await getValidatedRouterParams(event, z.string().uuid().parse) —
replace z.string().uuid() with z.uuid()) so the route parameter validation uses
z.uuid() for the id field.
server/api/documents/[id].delete.ts (1)

21-21: ⚡ Quick win

Deprecated z.string().uuid() — replace with z.uuid()

Same pattern as flagged in reject.post.ts. Replace z.string().uuid() with the Zod 4 standalone z.uuid().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/documents/`[id].delete.ts at line 21, The code uses the deprecated
z.string().uuid() when validating the route param; replace that call with the
Zod 4 standalone validator z.uuid() in the getValidatedRouterParams invocation
(so change the schema passed to getValidatedRouterParams to use z.uuid() for the
id field while keeping the existing destructuring into documentId and the
surrounding call site like getValidatedRouterParams(event, z.object({ id:
z.uuid() }).parse).
server/api/chatbot/folders/[id].patch.ts (1)

20-20: ⚡ Quick win

Same deprecated z.string().uuid() — replace with z.uuid()

Same pattern as server/api/join-requests/[id]/reject.post.ts. Switch to the shared routeIdSchema once extracted, or at minimum replace the inline call with z.object({ id: z.uuid() }).parse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chatbot/folders/`[id].patch.ts at line 20, The route parameter
validation uses the deprecated pattern z.string().uuid() inside the call to
getValidatedRouterParams; update the schema passed to getValidatedRouterParams
to use z.uuid() (or replace with the shared routeIdSchema if available) so the
line becomes getValidatedRouterParams(event, z.object({ id: z.uuid() }).parse)
or uses routeIdSchema.parse; update references in the handler where the { id }
is destructured to match the new schema.
server/api/invite-links/[id].delete.ts (1)

13-13: ⚡ Quick win

Deprecated z.string().uuid() — replace with z.uuid()

Same as the pattern flagged in reject.post.ts. Replace z.string().uuid() with z.uuid() (and ideally pull in the shared routeIdSchema).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/invite-links/`[id].delete.ts at line 13, The router params
validation uses deprecated z.string().uuid(); update the call that extracts
linkId (const { id: linkId } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid() }).parse)) to use z.uuid() instead—ideally replace the
inline schema with the shared routeIdSchema (or import routeIdSchema and pass
getValidatedRouterParams(event, routeIdSchema.parse)) so validation uses
z.uuid() and reuses the common schema; ensure the variable name linkId and the
call to getValidatedRouterParams remain unchanged.
server/utils/rateLimit.ts (1)

13-20: ⚡ Quick win

Startup-warning test exercises a copy of the logic, not the module itself

The test in tests/unit/security-fixes.test.ts (lines 309–340) reproduces the if (count > 1) branch inline instead of importing rateLimit.ts, so any future change to the threshold condition or message in the actual module would not be caught.

Module-level code is awkward to test because Node caches the module after the first import. The usual pattern to make it testable is to extract the check into an exported function:

♻️ Example extraction
-const _replicaCount = Number(process.env.RAILWAY_REPLICA_COUNT ?? 0)
-if (_replicaCount > 1) {
-  console.warn(...)
-}
+export function warnIfHorizontallyScaled(env: NodeJS.ProcessEnv = process.env): void {
+  const replicaCount = Number(env.RAILWAY_REPLICA_COUNT ?? 0)
+  if (replicaCount > 1) {
+    console.warn(
+      `[rateLimit] WARNING: RAILWAY_REPLICA_COUNT=${replicaCount}. ` +
+      'The in-memory rate limiter is NOT shared across replicas — effective limits are ' +
+      `${replicaCount}× higher than configured. Move rate limiting to the edge.`,
+    )
+  }
+}
+warnIfHorizontallyScaled()

The test can then call warnIfHorizontallyScaled({ RAILWAY_REPLICA_COUNT: '3' }) directly, verifying the real code path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/rateLimit.ts` around lines 13 - 20, Extract the startup check
out of the module-level code in rateLimit.ts into an exported function named
warnIfHorizontallyScaled(env?: Record<string,string>) that computes replicaCount
(currently derived from process.env.RAILWAY_REPLICA_COUNT/_replicaCount) and
emits the same console.warn when count > 1; replace the inline if-block with a
single call to warnIfHorizontallyScaled() so runtime behavior is unchanged but
tests can import and call warnIfHorizontallyScaled({ RAILWAY_REPLICA_COUNT: '3'
}) to exercise the branch.
server/api/chatbot/agents/[id].patch.ts (1)

28-28: ⚡ Quick win

Deprecated z.string().uuid() — replace with z.uuid()

Same pattern as the other endpoint files. Replace z.string().uuid() with z.uuid().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chatbot/agents/`[id].patch.ts at line 28, Replace the deprecated
schema usage in the router params: in the call to getValidatedRouterParams where
you build the Zod schema (currently using z.object({ id: z.string().uuid()
}).parse), switch to z.object({ id: z.uuid() }).parse so the id validation uses
z.uuid(); update only the schema expression referenced in the
getValidatedRouterParams(...) invocation and keep everything else (event
handling and variable destructuring from const { id }) unchanged.
server/api/join-requests/[id]/reject.post.ts (1)

13-13: ⚡ Quick win

z.string().uuid() is deprecated in Zod 4 — use z.uuid() instead

z.string().email(); // ❌ deprecatedz.email(); // ✅ — the same deprecation applies to all string-format method validators. The method equivalents (z.string().email(), etc.) are still available but have been deprecated and will be removed in the next major version.

This inline z.object({ id: z.string().uuid() }).parse schema is also duplicated verbatim across at least six endpoints. Consider centralising it in a shared util (e.g. server/utils/routeSchemas.ts) to eliminate drift:

♻️ Proposed fix (local + shared-schema approach)
// server/utils/routeSchemas.ts  (new or existing shared file)
+import { z } from 'zod'
+export const routeIdSchema = z.object({ id: z.uuid() })
// server/api/join-requests/[id]/reject.post.ts
-import { z } from 'zod'
+import { routeIdSchema } from '../../../utils/routeSchemas'
 ...
-  const { id: requestId } = await getValidatedRouterParams(event, z.object({ id: z.string().uuid() }).parse)
+  const { id: requestId } = await getValidatedRouterParams(event, routeIdSchema.parse)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/join-requests/`[id]/reject.post.ts at line 13, Replace the
deprecated z.string().uuid() usage and centralize the duplicated route schema:
create a shared schema (e.g., export requestIdSchema = z.object({ id: z.uuid()
}) from a new server/utils/routeSchemas.ts) and then update this handler to call
getValidatedRouterParams(event, requestIdSchema.parse) instead of the inline
z.object({ id: z.string().uuid() }).parse; update other endpoints to import and
reuse requestIdSchema to avoid duplication.
server/utils/pgDumpEnv.ts (1)

7-7: Consider adding PGSSLMODE (and friends) to the allowlist for remote-DB deployments

When DATABASE_URL includes SSL parameters (e.g. ?sslmode=require), the caller in backup.post.ts extracts host/port/user/db individually from the URL and passes them as CLI flags — it does not forward sslmode. Because PGSSLMODE is also absent from the allowlist here, pg_dump inherits no SSL hint and defaults to prefer (try SSL, accept plaintext). For self-hosted single-VPS setups (localhost DB) this is fine, but for any cloud-managed Postgres that enforces SSL it could silently back up over an unencrypted connection — or fail when the server rejects the unencrypted attempt.

-const PG_DUMP_ENV_ALLOWLIST = ['PATH', 'HOME', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'TMPDIR'] as const
+const PG_DUMP_ENV_ALLOWLIST = [
+  'PATH', 'HOME', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'TMPDIR',
+  // SSL negotiation (needed for remote/cloud DB with sslmode=require)
+  'PGSSLMODE', 'PGSSLROOTCERT', 'PGSSLCERT', 'PGSSLKEY',
+] as const

If PGSSLMODE should be derived from the DATABASE_URL query string rather than the parent env, the alternative is to parse dbUrl.searchParams.get('sslmode') in the caller and pass it explicitly as a PGSSLMODE override in the returned env.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/pgDumpEnv.ts` at line 7, PG_DUMP_ENV_ALLOWLIST omits PGSSLMODE
so pg_dump can lose SSL directives when DATABASE_URL is split into flags; update
the allowlist constant PG_DUMP_ENV_ALLOWLIST to include PGSSLMODE (and
optionally related vars like PGSSLCERT/PGSSLKEY/PGSSLROOTCERT) so pg_dump
inherits SSL settings, or alternatively parse sslmode from the DATABASE_URL in
backup.post.ts and explicitly set PGSSLMODE in the env returned to pg_dump;
modify PG_DUMP_ENV_ALLOWLIST and/or add code in the caller that reads
dbUrl.searchParams.get('sslmode') and injects PGSSLMODE into the process env
passed to pg_dump.
tests/unit/security-fixes.test.ts (2)

12-13: 💤 Low value

Duplicate import of randomBytes.

Both randomBytes and cryptoRandomBytes are imported from node:crypto, but they're the same function. Line 13's import shadows the one from line 12.

♻️ Remove duplicate import
-import { createHash, hkdfSync, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
-import { randomBytes as cryptoRandomBytes } from 'node:crypto'
+import { createHash, hkdfSync, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'

Then update line 39 to use randomBytes instead of cryptoRandomBytes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/security-fixes.test.ts` around lines 12 - 13, The file imports
randomBytes twice (as randomBytes and cryptoRandomBytes), causing a duplicate
shadowed import; remove the second import alias (cryptoRandomBytes) from the
import list and update all usages of cryptoRandomBytes to use randomBytes
instead (e.g., replace the call at the current test usage with randomBytes) so
only the single randomBytes import from node:crypto is used.

309-386: ⚖️ Poor tradeoff

Fix 5 tests reproduce logic locally instead of testing actual implementation.

These tests manually reproduce the warning logic from rateLimit.ts rather than importing and exercising the actual code. If the implementation in rateLimit.ts changes (e.g., different threshold, different message format), these tests will still pass while the real behavior diverges.

Consider importing and triggering the actual startup warning logic, or add integration tests that verify the real module's behavior. The current approach documents the expected contract but doesn't validate the implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/security-fixes.test.ts` around lines 309 - 386, Tests are
reproducing the warning logic locally instead of exercising the real code;
update the tests to import the actual rateLimit module and trigger its startup
path (rather than duplicating the string/threshold). Specifically, set
process.env.RAILWAY_REPLICA_COUNT to the desired value, spy on console.warn,
then require/import the module or call its exported startup function (e.g.,
initRateLimiter or checkReplicaCount) from rateLimit.ts so the real code emits
the warning; if no such export exists, add a small exported function
(checkReplicaCount or initRateLimiter) in rateLimit.ts that runs the existing
startup check so tests can call it directly. Ensure assertions then inspect
warnSpy calls for the real message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@server/api/public/jobs/`[slug]/apply.post.ts:
- Around line 16-21: The apply endpoint's rate limiter (applyRateLimit created
via createRateLimiter) blocks E2E tests; update the rate limiter initialization
to skip or relax limits when running tests by checking the environment (e.g.,
process.env.NODE_ENV === 'test' or a dedicated flag like process.env.E2E ===
'true') and only apply createRateLimiter in non-test environments, or set a much
higher maxRequests for test/e2e envs; modify the applyRateLimit definition so it
conditionally returns a no-op/mocked middleware for test runs while keeping the
existing 15min/5-request behavior for production.

---

Nitpick comments:
In `@package.json`:
- Line 91: The dependency spec for "uuid" currently allows any future major (
"uuid": ">=14.0.0" ); change it to bound the major version for reproducible
installs by restricting to the 14.x range (for example use a semver range like
"uuid": ">=14.0.0 <15.0.0" or simply "^14.0.0") so updates are limited to
non-breaking releases; update the "uuid" entry in package.json accordingly.

In `@server/api/candidates/`[id]/documents/index.post.ts:
- Line 39: Replace the deprecated z.string().uuid() inline schema with Zod 4's
z.uuid() and centralize the schema: create a shared exported constant (e.g.,
ROUTE_ID_PARAM_SCHEMA = z.object({ id: z.uuid() })) in a validation utility and
update calls that pass the inline parser (the getValidatedRouterParams call
currently using z.object({ id: z.string().uuid() }).parse) to use
ROUTE_ID_PARAM_SCHEMA.parse; update the same replacement across the ~14 route
files that duplicate the inline schema so future changes are one-liners.

In `@server/api/chatbot/agents/`[id].patch.ts:
- Line 28: Replace the deprecated schema usage in the router params: in the call
to getValidatedRouterParams where you build the Zod schema (currently using
z.object({ id: z.string().uuid() }).parse), switch to z.object({ id: z.uuid()
}).parse so the id validation uses z.uuid(); update only the schema expression
referenced in the getValidatedRouterParams(...) invocation and keep everything
else (event handling and variable destructuring from const { id }) unchanged.

In `@server/api/chatbot/folders/`[id].patch.ts:
- Line 20: The route parameter validation uses the deprecated pattern
z.string().uuid() inside the call to getValidatedRouterParams; update the schema
passed to getValidatedRouterParams to use z.uuid() (or replace with the shared
routeIdSchema if available) so the line becomes getValidatedRouterParams(event,
z.object({ id: z.uuid() }).parse) or uses routeIdSchema.parse; update references
in the handler where the { id } is destructured to match the new schema.

In `@server/api/documents/`[id].delete.ts:
- Line 21: The code uses the deprecated z.string().uuid() when validating the
route param; replace that call with the Zod 4 standalone validator z.uuid() in
the getValidatedRouterParams invocation (so change the schema passed to
getValidatedRouterParams to use z.uuid() for the id field while keeping the
existing destructuring into documentId and the surrounding call site like
getValidatedRouterParams(event, z.object({ id: z.uuid() }).parse).

In `@server/api/documents/`[id]/preview.get.ts:
- Line 26: The call that validates route params uses the deprecated schema
z.string().uuid(); update the validator to use z.uuid() instead by changing the
schema passed into getValidatedRouterParams (the line that assigns documentId
via const { id: documentId } = await getValidatedRouterParams(event,
z.string().uuid().parse) — replace z.string().uuid() with z.uuid()) so the route
parameter validation uses z.uuid() for the id field.

In `@server/api/invite-links/`[id].delete.ts:
- Line 13: The router params validation uses deprecated z.string().uuid();
update the call that extracts linkId (const { id: linkId } = await
getValidatedRouterParams(event, z.object({ id: z.string().uuid() }).parse)) to
use z.uuid() instead—ideally replace the inline schema with the shared
routeIdSchema (or import routeIdSchema and pass getValidatedRouterParams(event,
routeIdSchema.parse)) so validation uses z.uuid() and reuses the common schema;
ensure the variable name linkId and the call to getValidatedRouterParams remain
unchanged.

In `@server/api/join-requests/`[id]/reject.post.ts:
- Line 13: Replace the deprecated z.string().uuid() usage and centralize the
duplicated route schema: create a shared schema (e.g., export requestIdSchema =
z.object({ id: z.uuid() }) from a new server/utils/routeSchemas.ts) and then
update this handler to call getValidatedRouterParams(event,
requestIdSchema.parse) instead of the inline z.object({ id: z.string().uuid()
}).parse; update other endpoints to import and reuse requestIdSchema to avoid
duplication.

In `@server/utils/pgDumpEnv.ts`:
- Line 7: PG_DUMP_ENV_ALLOWLIST omits PGSSLMODE so pg_dump can lose SSL
directives when DATABASE_URL is split into flags; update the allowlist constant
PG_DUMP_ENV_ALLOWLIST to include PGSSLMODE (and optionally related vars like
PGSSLCERT/PGSSLKEY/PGSSLROOTCERT) so pg_dump inherits SSL settings, or
alternatively parse sslmode from the DATABASE_URL in backup.post.ts and
explicitly set PGSSLMODE in the env returned to pg_dump; modify
PG_DUMP_ENV_ALLOWLIST and/or add code in the caller that reads
dbUrl.searchParams.get('sslmode') and injects PGSSLMODE into the process env
passed to pg_dump.

In `@server/utils/rateLimit.ts`:
- Around line 13-20: Extract the startup check out of the module-level code in
rateLimit.ts into an exported function named warnIfHorizontallyScaled(env?:
Record<string,string>) that computes replicaCount (currently derived from
process.env.RAILWAY_REPLICA_COUNT/_replicaCount) and emits the same console.warn
when count > 1; replace the inline if-block with a single call to
warnIfHorizontallyScaled() so runtime behavior is unchanged but tests can import
and call warnIfHorizontallyScaled({ RAILWAY_REPLICA_COUNT: '3' }) to exercise
the branch.

In `@tests/unit/security-fixes.test.ts`:
- Around line 12-13: The file imports randomBytes twice (as randomBytes and
cryptoRandomBytes), causing a duplicate shadowed import; remove the second
import alias (cryptoRandomBytes) from the import list and update all usages of
cryptoRandomBytes to use randomBytes instead (e.g., replace the call at the
current test usage with randomBytes) so only the single randomBytes import from
node:crypto is used.
- Around line 309-386: Tests are reproducing the warning logic locally instead
of exercising the real code; update the tests to import the actual rateLimit
module and trigger its startup path (rather than duplicating the
string/threshold). Specifically, set process.env.RAILWAY_REPLICA_COUNT to the
desired value, spy on console.warn, then require/import the module or call its
exported startup function (e.g., initRateLimiter or checkReplicaCount) from
rateLimit.ts so the real code emits the warning; if no such export exists, add a
small exported function (checkReplicaCount or initRateLimiter) in rateLimit.ts
that runs the existing startup check so tests can call it directly. Ensure
assertions then inspect warnSpy calls for the real message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72170c91-b413-44f5-bba9-01c70ede3c0a

📥 Commits

Reviewing files that changed from the base of the PR and between f6fe4ad and 6fe4900.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (32)
  • ARCHITECTURE.md
  • SELF-HOSTING.md
  • app/app.vue
  • nuxt.config.ts
  • package.json
  • server/api/candidates/[id]/documents/index.post.ts
  • server/api/chatbot/agents/[id].delete.ts
  • server/api/chatbot/agents/[id].patch.ts
  • server/api/chatbot/conversations/[id].delete.ts
  • server/api/chatbot/conversations/[id].get.ts
  • server/api/chatbot/conversations/[id].patch.ts
  • server/api/chatbot/folders/[id].delete.ts
  • server/api/chatbot/folders/[id].patch.ts
  • server/api/documents/[id].delete.ts
  • server/api/documents/[id]/download.get.ts
  • server/api/documents/[id]/preview.get.ts
  • server/api/invite-links/[id].delete.ts
  • server/api/invite-links/info/[token].get.ts
  • server/api/join-requests/[id]/approve.post.ts
  • server/api/join-requests/[id]/reject.post.ts
  • server/api/public/jobs/[slug]/apply.post.ts
  • server/api/public/track/[code].get.ts
  • server/api/updates/backup.post.ts
  • server/middleware/csp.ts
  • server/utils/encryption.ts
  • server/utils/pgDumpEnv.ts
  • server/utils/rateLimit.ts
  • tests/unit/ai-config-schema.test.ts
  • tests/unit/auth-security-hardening.test.ts
  • tests/unit/pg-dump-env.test.ts
  • tests/unit/rate-limit.test.ts
  • tests/unit/security-fixes.test.ts

Co-authored-by: Copilot <copilot@github.com>
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-171 May 3, 2026 10:19 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/layouts/auth.vue`:
- Around line 12-13: The tooltip title for the dark-mode toggle is hardcoded;
update the title binding to use the app's i18n keys instead of raw English by
calling the translation helper with the appropriate keys (e.g., use $t or the
composition API's t) and pass the current state via isDark so the title becomes
something like t(isDark ? 'ui.switchToLight' : 'ui.switchToDark'); apply the
same change in the PublicNavBar component where the title is currently hardcoded
(referencing the same isDark and toggleColorMode bindings) so both components
use localized strings.

In `@server/plugins/csp-nonce.ts`:
- Around line 3-4: The current chained .replace calls on the HTML string only
replace empty nonce attributes and scripts missing a nonce, leaving pre-existing
non-empty nonce values unchanged; update the first .replace (the one targeting
nonce="" on the line with .replace(/<script\b([^>]*?)\snonce=""([^>]*?)>/g, ...)
to instead match any nonce attribute value (e.g., match nonce="...") so it
normalizes existing non-empty nonces to the per-request nonce, while keeping the
second .replace (the one matching scripts without any nonce) to insert a nonce
for scripts that don't have the attribute; locate these two chained .replace
calls in server/plugins/csp-nonce.ts and adjust the first regex to capture any
nonce attribute and replace it with nonce="${nonce}".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02bea2eb-fd77-4e37-9de5-4b5745db35f0

📥 Commits

Reviewing files that changed from the base of the PR and between 6fe4900 and 8068e4e.

📒 Files selected for processing (11)
  • app/app.vue
  • app/assets/css/main.css
  • app/components/PublicNavBar.vue
  • app/composables/useColorMode.ts
  • app/layouts/auth.vue
  • app/pages/index.vue
  • app/plugins/color-mode.client.ts
  • server/plugins/csp-nonce.ts
  • tests/tsconfig.json
  • tests/unit/rate-limit.test.ts
  • tests/unit/security-fixes.test.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/app.vue
  • tests/unit/security-fixes.test.ts
  • tests/unit/rate-limit.test.ts

Comment thread app/layouts/auth.vue
Comment on lines +12 to +13
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
@click="toggleColorMode"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize dark-mode toggle tooltip text.

Line 12 uses hardcoded English ("Switch to light mode" / "Switch to dark mode"), which bypasses i18n. The same pattern is also present in app/components/PublicNavBar.vue Line 59.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/layouts/auth.vue` around lines 12 - 13, The tooltip title for the
dark-mode toggle is hardcoded; update the title binding to use the app's i18n
keys instead of raw English by calling the translation helper with the
appropriate keys (e.g., use $t or the composition API's t) and pass the current
state via isDark so the title becomes something like t(isDark ?
'ui.switchToLight' : 'ui.switchToDark'); apply the same change in the
PublicNavBar component where the title is currently hardcoded (referencing the
same isDark and toggleColorMode bindings) so both components use localized
strings.

Comment on lines +3 to +4
.replace(/<script\b([^>]*?)\snonce=""([^>]*?)>/g, `<script$1 nonce="${nonce}"$2>`)
.replace(/<script\b(?![^>]*\snonce=)/g, `<script nonce="${nonce}"`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize all existing nonce attributes, not just empty ones.

On Line 3 and Line 4, scripts with a pre-existing non-empty nonce are skipped and keep their old value, which can mismatch the per-request CSP nonce and block execution.

Proposed fix
 function addNonceToScripts(markup: string, nonce: string) {
   return markup
-    .replace(/<script\b([^>]*?)\snonce=""([^>]*?)>/g, `<script$1 nonce="${nonce}"$2>`)
+    .replace(/<script\b([^>]*?)\snonce=(['"])[^'"]*\2([^>]*?)>/g, `<script$1 nonce="${nonce}"$3>`)
     .replace(/<script\b(?![^>]*\snonce=)/g, `<script nonce="${nonce}"`)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.replace(/<script\b([^>]*?)\snonce=""([^>]*?)>/g, `<script$1 nonce="${nonce}"$2>`)
.replace(/<script\b(?![^>]*\snonce=)/g, `<script nonce="${nonce}"`)
.replace(/<script\b([^>]*?)\snonce=(['"])[^'"]*\2([^>]*?)>/g, `<script$1 nonce="${nonce}"$3>`)
.replace(/<script\b(?![^>]*\snonce=)/g, `<script nonce="${nonce}"`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/plugins/csp-nonce.ts` around lines 3 - 4, The current chained .replace
calls on the HTML string only replace empty nonce attributes and scripts missing
a nonce, leaving pre-existing non-empty nonce values unchanged; update the first
.replace (the one targeting nonce="" on the line with
.replace(/<script\b([^>]*?)\snonce=""([^>]*?)>/g, ...) to instead match any
nonce attribute value (e.g., match nonce="...") so it normalizes existing
non-empty nonces to the per-request nonce, while keeping the second .replace
(the one matching scripts without any nonce) to insert a nonce for scripts that
don't have the attribute; locate these two chained .replace calls in
server/plugins/csp-nonce.ts and adjust the first regex to capture any nonce
attribute and replace it with nonce="${nonce}".

…roduction

Co-authored-by: Copilot <copilot@github.com>
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-171 May 3, 2026 10:43 Destroyed
@JoachimLK JoachimLK merged commit bfb4483 into main May 3, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant