Skip to content

feat(backend): implement payload sanitization and strict validation for Database Row-Level Security Service - #1609

Open
Abidoyesimze wants to merge 1 commit into
emdevelopa:mainfrom
Abidoyesimze:feature/be-implement-payload-sanitization-and-strict-validation-for-database-row-level-security-service
Open

Abidoyesimze wants to merge 1 commit into
emdevelopa:mainfrom
Abidoyesimze:feature/be-implement-payload-sanitization-and-strict-validation-for-database-row-level-security-service

Conversation

@Abidoyesimze

Copy link
Copy Markdown
Contributor

Summary

Closes #1467

withMerchantContext() in src/lib/db-rls.js passed merchantId straight into SET LOCAL app.current_merchant_id — anything truthy was accepted, including empty-after-trim strings, 10k-character payloads and strings with spaces/semicolons. The rollback path could also mask the original error if ROLLBACK itself failed, and a non-function callback crashed mid-transaction.

Changes:

  • Strict Zod schema for merchantId: trimmed, 1–128 chars, character class [a-zA-Z0-9_\-:.@]. Invalid IDs fail fast with a clear error before a connection is taken from the pool, so bad input can't hold a pooled client while it fails.
  • validateAndSanitizeMerchantId() — single validation/sanitization entry point returning { valid, merchantId } or { valid, error }.
  • sanitizeMerchantId() — non-throwing helper for safe log enrichment (returns null rather than throwing on garbage input).
  • Callback type check — a non-function callback fails deterministically before connecting instead of exploding at callback(client) inside the transaction.
  • Guarded rollback — ROLLBACK is wrapped so a rollback failure is logged rather than replacing the original error; the client is always released via finally.

Test plan

npx vitest run src/lib/db-rls.test.js
 Test Files  1 passed (1)
      Tests  19 passed (19)

New src/lib/db-rls.test.js (19 tests): accepted IDs (alphanumeric, UUIDs, emails, underscore/hyphen/dot/colon/at), rejected IDs (empty, whitespace-only, over 128 chars, spaces, quotes, semicolons, null/undefined/numbers/arrays), trim normalization, callback type rejection, and full transaction lifecycle — pool.connect → BEGIN → SET LOCAL → callback → COMMIT → release ordering, rollback-on-callback-error with release, rollback-failure not masking the original error, and connection-error propagation without leaking a client.

Also passes: src/lib/metricService.test.js (the main consumer of withMerchantContext, with the module mocked — no behavioural change to its contract).

Full backend suite: no new failures vs. origin/main (identical pre-existing failure count).

Security notes

  • This is the RLS chokepoint: merchantId becomes the Postgres app.current_merchant_id GUC that row-level security policies filter on. Unvalidated input here is a policy-bypass vector — a crafted ID could, depending on how policies compare the GUC, match more rows than intended, and oversized payloads were a cheap memory/CPU DoS against the pool.
  • The character class is an allowlist, not a blocklist: only identifier-shaped values pass; SQL metacharacters, quotes, whitespace and control characters are rejected before any query is constructed.
  • Validation errors log the offending value at warn level only for the rejection reason — the value itself is included for operator debugging but never reaches metrics or health output.
  • Sanitization is applied to the value that goes into the SET LOCAL parameter and the value used for logging, so both paths use the same normalized ID.

…or Database Row-Level Security Service

withMerchantContext previously passed merchantId straight to SET LOCAL accepting
anything truthy, and the rollback path could mask the original error if ROLLBACK
itself failed:

- Adds a strict Zod schema for merchantId (length bounds + allowed character
  class) applied before any SQL runs; invalid IDs fail fast with a clear error
  before a connection is taken from the pool.
- Sanitizes/normalizes the ID (trim + strip disallowed characters) for both the
  transaction and logging paths, and exposes sanitizeMerchantId for safe log
  enrichment.
- Validates the callback is actually a function, so a bad call site fails
  deterministically instead of crashing mid-transaction.
- Rollback is now wrapped so a rollback failure is logged rather than replacing
  the original error, and the client is always released.

Adds unit tests covering accepted/rejected IDs, sanitization edge cases, and
transaction commit/rollback/release ordering.
@vercel

vercel Bot commented Oct 17, 2026

Copy link
Copy Markdown

@Abidoyesimze is attempting to deploy a commit to the Emmanuel's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@Abidoyesimze Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

This branch has not been deployed

No deployments
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.

[Backend] Implement payload sanitization and strict validation for Database Row-Level Security Service

1 participant