All Docs
FeaturesCalmony PayUpdated March 15, 2026

Security: Hardening the x-org-id Cookie (SEC-16)

Security Deep-Dive: Hardening the x-org-id Cookie (SEC-16)

Release: v1.0.49 · Control: SEC-16 · Severity: High

Background

Calmony Pay is a multi-tenant API. Every request must be resolved to a specific organisation so that payment records, invoices, and settlement accounts are scoped correctly. To carry this context across requests, the platform uses an x-org-id cookie that is read inside createTRPCContext on every tRPC call.

Prior to v1.0.49, this cookie was written from client-side JavaScript in src/components/org-provider.tsx, and no explicit cookie security attributes were set.

What Was the Risk?

1. JavaScript Tampering (Missing HttpOnly)

Without the HttpOnly flag, any JavaScript running in the browser — including third-party scripts, browser extensions, or injected code via an XSS vulnerability — could read or overwrite the x-org-id cookie:

// Previously possible by any script on the page
document.cookie = 'x-org-id=another-org-uuid; path=/';

If an attacker could set this value to a different organisation's ID, subsequent tRPC calls might resolve and return that organisation's data, depending on how session validation was layered.

2. CSRF Exposure (Missing SameSite)

Without a SameSite attribute, browsers default to SameSite=Lax (or None in older browsers). Under Lax, cross-site navigations that trigger top-level GET requests will still send the cookie. A malicious page could construct a request that carries a forged x-org-id value alongside the user's real session cookie, potentially causing org-context confusion on state-changing operations.

3. Plaintext Transmission Risk (Missing Secure)

Without the Secure flag there is no browser-enforced guarantee that the cookie is only transmitted over HTTPS, leaving it visible on any unencrypted connection.

What Changed in v1.0.49

The x-org-id cookie is no longer written from org-provider.tsx using client-side JavaScript. It is now issued from a server-side context (a Next.js server action or API route) with all three security attributes explicitly set:

// Server action / API route — server-side only
cookies().set('x-org-id', resolvedOrgId, {
  httpOnly: true,   // not readable by JavaScript
  secure: true,     // HTTPS only
  sameSite: 'strict', // no cross-site sending
  path: '/',
});

Why SameSite=Strict?

Strict is the most conservative option. The cookie is never sent on a cross-site request, including top-level navigations originating from another domain. This is appropriate here because:

  • Org-context resolution is not needed for cross-site link clicks.
  • The cookie carries a security-sensitive tenant identifier, not a display preference.
  • Strict mode eliminates the entire cross-site cookie-leakage surface.

Defence in Depth

In addition to the cookie attribute changes, the recommended posture for createTRPCContext is to validate the x-org-id value against the authenticated session rather than trusting it unconditionally. For example:

// createTRPCContext — illustrative validation pattern
const session = await getServerSession();
const cookieOrgId = cookies().get('x-org-id')?.value;

// Confirm the user is actually a member of the org in the cookie
const org = await db.orgMembership.findFirst({
  where: { orgId: cookieOrgId, userId: session.user.id },
});

if (!org) {
  throw new TRPCError({ code: 'FORBIDDEN' });
}

This ensures that even if a cookie value were somehow forged, the session-level membership check would block access to the wrong organisation's data.

Summary of Changes

WhatBefore v1.0.49From v1.0.49
Cookie written fromClient-side JS (org-provider.tsx)Server action / API route
HttpOnly❌ Not settrue
Secure❌ Not settrue
SameSite❌ Not set (browser default)Strict
JS-accessible✅ Yes❌ No
Cross-site leakage⚠️ Possible❌ Blocked

Affected File

  • src/components/org-provider.tsx — client-side cookie write removed; org context now flows from server-issued cookie.