Skip to content

Security: pg_dump env allowlist + uuid CVE override + tests#170

Closed
davebulaval wants to merge 5 commits into
reqcore-inc:mainfrom
Baseline-quebec:upstream/security-fixes
Closed

Security: pg_dump env allowlist + uuid CVE override + tests#170
davebulaval wants to merge 5 commits into
reqcore-inc:mainfrom
Baseline-quebec:upstream/security-fixes

Conversation

@davebulaval

@davebulaval davebulaval commented May 2, 2026

Copy link
Copy Markdown

Summary

Four small, focused commits hardening Reqcore's security posture without changing any user-facing behavior. All commits DCO-signed.

1. fix(security) — drop application secrets from pg_dump child env

/api/updates/backup previously spawned pg_dump with { ...process.env, PGPASSWORD: ... }, copying every parent secret (BETTER_AUTH_SECRET, S3_SECRET_KEY, OAuth credentials, etc.) into the subprocess. Any of these could leak through pg_dump's stderr or a libpq diagnostic.

Replaces the spread with an explicit allowlist (PATH, HOME, LANG, LC_ALL, LC_CTYPE, TZ, TMPDIR, plus PGPASSWORD) extracted into server/utils/pgDumpEnv.ts so it can be unit-tested without spawning pg_dump. New regression test asserts none of the well-known Reqcore secrets reach the child env.

Note for maintainers: I held off on a full coordinated disclosure via security@reqcore.com because there's no production exposure path that's exploitable without owner-level org access (the route is gated by requirePermission(event, { organization: ['delete'] })) — but happy to coordinate timing on this commit if you'd prefer to merge the others first.

2. fix(deps) — override uuid to >=14.0.0 (GHSA-w5hq-g745-h8pq)

uuid v3/v5/v6 accept caller-provided output buffers without bounds checks, allowing silent partial writes. Reqcore pulls uuid transitively through @better-auth/sso > samlify and resend > svix. Adding the override forces both to 14.0.0. No production code in this repo reads uuid directly, but the override closes the audit finding cleanly.

3. test — cover rate limiter logic and tighten AI config schema tests

  • tests/unit/rate-limit.test.ts (new, 8 tests): admit/block, per-IP isolation, per-limiter isolation, headers, Retry-After, sliding window with fake timers, and an X-Forwarded-For anti-spoof regression test.
  • tests/unit/ai-config-schema.test.ts (rewritten, 14 tests): four tests in this file were silently failing on main because they omitted the required name field, plus one that exercised behavior belonging to updateAiConfigSchema. New coverage: required fields, trim, length ceilings, AWS + GCP metadata SSRF, max-tokens ceiling, updateAiConfigSchema apiKey-optional path.
  • server/utils/rateLimit.ts: dropped the runtime warning that pointed at "use Redis at scale". Reqcore is single-instance by design, so the right answer for horizontal scaling is to terminate at the edge. The docstring now says so explicitly and points to SELF-HOSTING.md.

4. docs — cover rate-limit scaling and pg_dump secret hardening

  • ARCHITECTURE.md: list server/plugins/posthog.ts and server/utils/pgDumpEnv.ts; clarify createRateLimiter is in-memory single-instance.
  • SELF-HOSTING.md: new "Scaling horizontally" subsection (terminate rate limiting at Cloudflare WAF / Caddy rate_limit / nginx limit_req); new bullet "Backups never leak app secrets".

Test plan

  • npx vue-tsc --noEmit clean
  • npx vitest run — 326/326 passing on this branch (the 4 previously-failing ai-config-schema tests are part of this PR's fix)
  • DCO sign-off on all four commits
  • Maintainers to confirm whether the pg_dump commit should land separately via the security-disclosure path

Checklist

  • Scoped to one concern (security hardening) — split into 4 atomic commits.
  • Tested locally (Docker stack + dev server + full vitest run).
  • Updated docs when behavior or policy changed (ARCHITECTURE, SELF-HOSTING).
  • No tenant-scope or auth regressions (the pg_dump route still gates on requirePermission).
  • All commits DCO signed (git commit -s).

Summary by CodeRabbit

  • Security

    • Database backups now securely isolate sensitive environment variables, preventing accidental exposure via backup output.
  • Documentation

    • Enhanced self-hosting guide with security best practices (rate limiting, headers, access control) and horizontal scaling guidance.
    • Updated architecture documentation to clarify server utilities and in-memory rate limiter behavior across instances.
  • Tests

    • Added comprehensive test coverage for rate limiting and backup environment handling.
  • Dependencies

    • Updated uuid dependency constraint.

The /api/updates/backup endpoint spawned pg_dump with `{ ...process.env,
PGPASSWORD: ... }`, copying every parent secret (BETTER_AUTH_SECRET,
S3_SECRET_KEY, OAuth credentials, etc.) into the subprocess. Any of
these could leak through pg_dump's stderr or a libpq diagnostic.

Replace the spread with an explicit allowlist (PATH, HOME, LANG,
LC_ALL, LC_CTYPE, TZ, TMPDIR, plus PGPASSWORD) extracted into
server/utils/pgDumpEnv.ts so it can be unit-tested without spawning
pg_dump. Adds a regression test that asserts none of the well-known
Reqcore secrets reach the child env.

Signed-off-by: davebulaval <david.beauchemin@baseline.quebec>
uuid v3/v5/v6 accept caller-provided output buffers without bounds
checks, allowing silent partial writes. v4/v1/v7 already throw
RangeError on invalid bounds; the fix in 14.0.0 brings the same
guard to v3/v5/v6.

Reqcore pulls uuid transitively through:
  - @better-auth/sso > samlify (uuid@8.3.2)
  - resend > svix (uuid@10.0.0)

Adding the override forces both to 14.0.0. No production code in
this repo reads from uuid directly, but the fix closes the audit
finding cleanly.

Signed-off-by: davebulaval <david.beauchemin@baseline.quebec>
Two test additions, one fix to a doc comment.

tests/unit/rate-limit.test.ts (new, 8 tests):
  - admits up to maxRequests, blocks the next request
  - per-IP isolation
  - per-limiter isolation (each createRateLimiter() owns its Map)
  - X-RateLimit-{Limit,Remaining,Reset} headers
  - Retry-After on rejection
  - sliding window slides (fake timers)
  - X-Forwarded-For honored only when TRUSTED_PROXY_IP matches the
    socket peer (anti-spoof regression test)

tests/unit/ai-config-schema.test.ts (rewritten, 14 tests):
  Replaces 4 tests that were silently failing because they omitted
  the required `name` field, plus one that exercised behavior that
  belongs to updateAiConfigSchema. New coverage: required fields,
  trim, length ceilings, both AWS and GCP metadata SSRF, max-tokens
  ceiling, updateAiConfigSchema apiKey-optional path.

server/utils/rateLimit.ts:
  - drop the runtime warning that pointed at "use Redis at scale";
    Reqcore is single-instance by design, so the right answer for
    horizontal scaling is to terminate at the edge (Cloudflare WAF /
    Caddy rate_limit / nginx limit_req). The docstring now says so
    explicitly and points to SELF-HOSTING.md.

Signed-off-by: davebulaval <david.beauchemin@baseline.quebec>
ARCHITECTURE.md
  - list the existing server/plugins/posthog.ts plugin
  - list server/utils/pgDumpEnv.ts
  - clarify createRateLimiter is in-memory single-instance

SELF-HOSTING.md
  - new "Scaling horizontally" subsection: Reqcore is single-instance
    by design; for multi-replica deployments terminate rate limiting
    at Cloudflare WAF / Caddy rate_limit / nginx limit_req
  - bullet "Backups never leak app secrets" describing the pg_dump env
    allowlist

Signed-off-by: davebulaval <david.beauchemin@baseline.quebec>
@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@davebulaval has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 11 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3397ecd-d0d9-4afc-953c-5e4763d65922

📥 Commits

Reviewing files that changed from the base of the PR and between 5ffb56c and 8c9d9c6.

📒 Files selected for processing (2)
  • server/utils/pgDumpEnv.ts
  • tests/unit/pg-dump-env.test.ts
📝 Walkthrough

Walkthrough

The PR adds security hardening for database backups via an allowlisted environment builder, documents rate limiter behavior as in-memory and suitable only for single instances, enhances AI configuration schema validation tests, and introduces comprehensive test coverage for rate limiting and pg_dump security. A uuid dependency override is also added.

Changes

pg_dump Security Hardening

Layer / File(s) Summary
Utility Definition
server/utils/pgDumpEnv.ts
New module exports buildPgDumpEnv() that constructs a restricted child process environment for pg_dump, forwarding only PGPASSWORD and allowlisted system variables to prevent unrelated application secrets from leaking to the subprocess.
Integration
server/api/updates/backup.post.ts
Updated backup endpoint to call buildPgDumpEnv(process.env, dbUrl.password) instead of spreading the full process.env, with inline documentation explaining the secret-prevention rationale.
Testing & Documentation
tests/unit/pg-dump-env.test.ts, ARCHITECTURE.md, SELF-HOSTING.md
New test suite validates that application secrets are dropped, PGPASSWORD is forwarded, allowlisted system vars are included, and parent env is not mutated. Architecture and self-hosting guides document the allowlist utility and backup security improvements.

Rate Limiter Documentation & Testing

Layer / File(s) Summary
Implementation Documentation
server/utils/rateLimit.ts
Updated JSDoc for createRateLimiter to clarify per-process, per-limiter in-memory bucket isolation and provide guidance for horizontal scaling via edge-based rate limiting (Cloudflare, Caddy, Nginx examples). Removed production-time console.warn about in-memory behavior.
Guidance Documentation
ARCHITECTURE.md, SELF-HOSTING.md
Refined createRateLimiter description in architecture docs; added new Scaling horizontally section in self-hosting docs explaining single-instance design and recommending edge-level rate limiting for multi-replica deployments.
Testing
tests/unit/rate-limit.test.ts
New comprehensive test suite verifies request admission/blocking, per-IP bucket isolation, instance isolation, response headers (X-RateLimit-*, Retry-After), custom error messages, time-window behavior with fake timers, and trusted proxy IP validation for X-Forwarded-For forwarding.

AI Configuration Schema Validation

Layer / File(s) Summary
Test Enhancement
tests/unit/ai-config-schema.test.ts
Expanded test coverage for both createAiConfigSchema and updateAiConfigSchema validators with additional assertions: SSRF rejection for GCP metadata endpoints, field requirement/constraint validation (name length, apiKey presence), whitespace trimming, maxTokens ceiling enforcement, and PATCH semantics allowing optional fields.

Dependency Override

Layer / File(s) Summary
Configuration
package.json
Added override constraint "uuid": ">=14.0.0" in the overrides section to enforce minimum uuid version.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #110: Introduces the backup API endpoint (server/api/updates/backup.post.ts) that this PR now harddens by restricting pg_dump environment variables.
  • PR #138: Previously added production-time console.warn to createRateLimiter; this PR removes that warning and refines documentation around in-memory rate-limit behavior.

Poem

🐰 Secrets stay safe in the backup's care,
Rate limits hold steady, single-instance fair,
Tests validate, docs clarify why—
Allowlists whisper, no secrets fly!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows Conventional Commits format and accurately summarizes the main security-focused changes: pg_dump environment allowlisting, uuid CVE override, and test additions.
Description check ✅ Passed Description is comprehensive and follows the template structure with clear Summary, Type of change, Validation, and DCO sections; all required information is present and well-detailed.
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 unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 0/1 reviews remaining, refill in 37 minutes and 11 seconds.

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

@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: 1

🧹 Nitpick comments (1)
tests/unit/ai-config-schema.test.ts (1)

124-134: ⚡ Quick win

Add a whitespace-only name case to surface the trim-ordering defect in the schema.

With z.string().min(1).trim() (validators before transform), an input of ' ' (spaces) passes min(1) because the check runs on the 3-character untrimmed string, then trim executes and returns ''. The upstream schema in server/utils/schemas/scoring.ts uses exactly z.string().min(1).max(80).trim(), so a whitespace-only name currently slips through validation and would be stored as an empty string. The existing trim test (line 125) only exercises the happy path (' Padded ') and won't catch this.

Adding the case below would surface the gap:

🧪 Suggested additional test case
  it('trims surrounding whitespace from name', () => {
    ...
  })

+ it('rejects a whitespace-only name after trimming', () => {
+   const result = createAiConfigSchema.safeParse({
+     name: '   ',
+     provider: 'openai',
+     model: 'gpt-4',
+     apiKey: 'k',
+   })
+
+   // z.string().min(1).max(80).trim() processes min(1) on the raw value,
+   // so '   ' currently passes and trims to ''. This test should fail
+   // until the schema is fixed to z.string().trim().min(1).max(80).
+   expect(result.success).toBe(false)
+ })

The schema fix (outside this file) would be to swap the chain order in server/utils/schemas/scoring.ts:

- name: z.string().min(1).max(80).trim(),
+ name: z.string().trim().min(1).max(80),

(Same adjustment applies to updateAiConfigSchema.name.)

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

In `@tests/unit/ai-config-schema.test.ts` around lines 124 - 134, Add a test to
ai-config-schema.test.ts using createAiConfigSchema.safeParse with name: '   '
(spaces) and assert result.success is false to catch the whitespace-only case;
then fix the schema ordering in server/utils/schemas/scoring.ts by calling
trim() before validators (i.e., use z.string().trim().min(1).max(80) for the
name field) and apply the same change to updateAiConfigSchema.name so validation
runs on the trimmed value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/utils/pgDumpEnv.ts`:
- Around line 18-25: The buildPgDumpEnv function currently always sets
PGPASSWORD to the provided password, which can emit an empty string and override
~/.pgpass; update buildPgDumpEnv to only add PGPASSWORD to childEnv when
password is a non-empty string (e.g. if (password) childEnv.PGPASSWORD =
password) and leave it unset otherwise, keeping the existing loop for
PG_DUMP_ENV_ALLOWLIST; also add a test case to pg-dump-env.test.ts (the
"compact" test) that passes password === '' and asserts that PGPASSWORD is not
present in the returned env.

---

Nitpick comments:
In `@tests/unit/ai-config-schema.test.ts`:
- Around line 124-134: Add a test to ai-config-schema.test.ts using
createAiConfigSchema.safeParse with name: '   ' (spaces) and assert
result.success is false to catch the whitespace-only case; then fix the schema
ordering in server/utils/schemas/scoring.ts by calling trim() before validators
(i.e., use z.string().trim().min(1).max(80) for the name field) and apply the
same change to updateAiConfigSchema.name so validation runs on the trimmed
value.
🪄 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: 4b07334b-1ed2-4b23-8395-016cc596774e

📥 Commits

Reviewing files that changed from the base of the PR and between f6fe4ad and 5ffb56c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • ARCHITECTURE.md
  • SELF-HOSTING.md
  • package.json
  • server/api/updates/backup.post.ts
  • server/utils/pgDumpEnv.ts
  • server/utils/rateLimit.ts
  • tests/unit/ai-config-schema.test.ts
  • tests/unit/pg-dump-env.test.ts
  • tests/unit/rate-limit.test.ts

Comment thread server/utils/pgDumpEnv.ts
An empty PGPASSWORD value can confuse older libpq versions and overrides
~/.pgpass / trust-auth lookup. Skip setting the variable entirely when
DATABASE_URL has no password component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: davebulaval <david.beauchemin@baseline.quebec>
@JoachimLK

Copy link
Copy Markdown
Contributor

Hey, really appreciate the effort you put into this, the pg_dump fix in particular is exactly the kind of thing that's easy to miss but matters a lot.

I've gone through everything and folded it all into #171 along with a few extra security improvements on my end. That PR is now merged so this one can be closed.

On the disclosure question, totally agree, the route is locked down to org owners so there's no real exposure path here. No need for a separate process.

Thanks again, this was a genuinely useful contribution. 🙏

@JoachimLK JoachimLK closed this May 3, 2026
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.

2 participants