Skip to content

Fix SSR Apollo cookie overwrite in server-side GraphQL requests#3985

Open
xDipzz wants to merge 7 commits intoOWASP:mainfrom
xDipzz:fix/3940-ssr-cookie-header-merge
Open

Fix SSR Apollo cookie overwrite in server-side GraphQL requests#3985
xDipzz wants to merge 7 commits intoOWASP:mainfrom
xDipzz:fix/3940-ssr-cookie-header-merge

Conversation

@xDipzz
Copy link

@xDipzz xDipzz commented Feb 18, 2026

Proposed change

Resolves #3940

This PR fixes SSR Apollo cookie propagation so authenticated context is preserved during server-side GraphQL requests.

What changed

  • Added frontend/src/server/apolloCookieHeader.ts with utilities to merge cookies safely and keep exactly one csrftoken value.
  • Updated frontend/src/server/apolloClient.ts to build request cookies from next/headers (cookies().getAll()), then merge with server CSRF token via mergeApolloHeadersWithCsrf(...).
  • Added regression tests in frontend/__tests__/unit/server/apolloCookieHeader.test.ts for:
    • preserving existing session/auth cookies,
    • replacing conflicting csrftoken without duplicates,
    • case robustness for Cookie/cookie,
    • null-CSRF behavior,
    • preserving non-cookie headers.

Verification

  • Ran:
    • pnpm --dir frontend exec jest __tests__/unit/server/apolloCookieHeader.test.ts --runInBand --coverage=false
  • Result:
    • PASS (6/6 tests)

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

No actionable comments were generated in the recent review. 🎉


Summary by CodeRabbit

Release Notes

  • Tests

    • Added unit tests for server-side cookie header utilities covering CSRF token handling and header consolidation scenarios.
  • New Features

    • Improved server-side cookie header computation and CSRF token management for authentication requests.

Walkthrough

Adds utilities to merge Cookie and CSRF headers for server-side Apollo requests, refactors the Apollo client to use them and exposes a helper to build the server Cookie header, and adds unit tests covering cookie/CSRF header merge behaviors and edge cases (duplicates, missing token, preserving other headers).

Changes

Cohort / File(s) Summary
Cookie Header Utilities
frontend/src/server/apolloCookieHeader.ts
Adds mergeCookieHeader and mergeApolloHeadersWithCsrf to parse, normalize and merge Cookie headers with a CSRF token and set X-CSRFToken.
Apollo Client Integration
frontend/src/server/apolloClient.ts
Refactors server auth link to call mergeApolloHeadersWithCsrf, adds and exports getRequestCookieHeaderOnServer to build a Cookie header string from server request cookies.
Unit Tests
frontend/__tests__/unit/server/apolloCookieHeader.test.ts
Adds tests covering merging incoming cookies with CSRF, creating CSRF when none exist, replacing existing csrftoken, consolidating duplicate Cookie fields, preserving cookies when CSRF missing, and keeping non-cookie headers intact.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • arkid15r
  • kasya
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 The title accurately describes the main change: fixing SSR Apollo cookie overwrite in server-side GraphQL requests, which directly matches the core problem and solution in the changeset.
Description check ✅ Passed The description clearly explains the problem, changes made, and verification performed, all directly related to the changeset and the linked issue #3940.
Linked Issues check ✅ Passed The PR fully addresses issue #3940: preserves incoming cookies, merges CSRF token safely without duplication, maintains X-CSRFToken, and includes comprehensive regression tests covering all acceptance criteria.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the SSR Apollo cookie header merge problem: new utility file, updated apolloClient integration, and regression tests with no extraneous modifications.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

cubic-dev-ai[bot]
cubic-dev-ai bot previously approved these changes Feb 18, 2026
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 3 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (3)
frontend/__tests__/unit/server/apolloCookieHeader.test.ts (1)

3-71: LGTM — good coverage of the happy paths and edge cases.

The 6 tests collectively cover: lowercase key, undefined headers, multi-token deduplication, case mixing, null CSRF, and non-cookie header preservation. One gap to consider adding:

Missing: test for csrfToken = '' (empty string). The !csrfToken guard in mergeCookieHeader treats '' identically to null — existing cookies (including a stale csrftoken) are preserved while X-CSRFToken is set to ''. Without a test, this behavior is undocumented and unprotected against future refactoring.

💡 Suggested additional test
+  test('treats empty string csrf token same as null: preserves existing cookies without replacing csrftoken', () => {
+    const headers = mergeApolloHeadersWithCsrf(
+      {
+        Cookie: 'sessionid=session123; csrftoken=oldvalue',
+      },
+      ''
+    )
+
+    // '' is falsy → mergeCookieHeader early-returns; stale csrftoken is NOT replaced
+    expect(headers.Cookie).toBe('sessionid=session123; csrftoken=oldvalue')
+    expect(headers['X-CSRFToken']).toBe('')
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/server/apolloCookieHeader.test.ts` around lines 3 -
71, Missing unit test for csrfToken = '': add a test case to ensure
mergeCookieHeader and mergeApolloHeadersWithCsrf treat an empty-string CSRF
token like null (preserve existing cookies including any stale csrftoken and set
X-CSRFToken to ''). Add a new test similar to the "preserves incoming cookies
when csrf token is missing" case but pass '' as the csrf argument, assert that
the Cookie header remains unchanged (including any existing csrftoken) and that
headers['X-CSRFToken'] === '' and that no duplicate csrftoken entries are
created (use the existing mergeCookieHeader helper in the test to validate
deduplication).
frontend/src/server/apolloClient.ts (2)

9-10: Two independent cookies() calls per request — consider sharing the cookie store.

Both getCsrfTokenOnServer and getRequestCookieHeaderOnServer each call await cookies() per invocation, resulting in two calls per GraphQL request inside the setContext callback. While functionally safe (both see the same immutable request context), it is redundant.

♻️ Proposed refactor — read cookie store once
-    const csrfToken = await getCsrfTokenOnServer()
-    const requestCookieHeader = await getRequestCookieHeaderOnServer()
+    const cookieStore = await cookies()
+    const csrfCookie = cookieStore.get('csrftoken')
+    const csrfToken = csrfCookie?.value ?? await fetchCsrfTokenServer()
+    const requestCookieHeader = cookieStore
+      .getAll()
+      .map(({ name, value }) => `${name}=${value}`)
+      .join('; ')

This keeps the two exported helpers intact for external consumers while avoiding the duplicate cookies() call inside the hot auth-link path.

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

In `@frontend/src/server/apolloClient.ts` around lines 9 - 10, Both
getCsrfTokenOnServer and getRequestCookieHeaderOnServer individually call await
cookies(), causing two redundant cookies() reads per request in the Apollo
setContext path; fix by reading the cookie store once inside the setContext
callback (call cookies() once), then pass that single cookie store into both
helpers (either by adding an optional parameter to
getCsrfTokenOnServer/getRequestCookieHeaderOnServer or by creating an internal
helper that accepts the cookie store) so the exported helpers remain unchanged
for external callers but the hot path re-uses the same cookie store.

52-59: getRequestCookieHeaderOnServer is exported but used only internally.

It's called only within createApolloClient in the same file. Removing the export would reduce unnecessary public API surface.

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

In `@frontend/src/server/apolloClient.ts` around lines 52 - 59, The helper
getRequestCookieHeaderOnServer is exported but only used internally by
createApolloClient; remove the unnecessary export to shrink the public API
surface by changing getRequestCookieHeaderOnServer to a module-private function
(e.g., drop the export keyword or convert it to a non-exported const/async
function) and keep its call sites (createApolloClient) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/server/apolloCookieHeader.ts`:
- Around line 20-22: Replace the falsy check on csrfToken with an explicit null
comparison so only a null value triggers the early return: change the condition
from if (!csrfToken) to if (csrfToken === null) in the code path that currently
returns existingCookies.join('; '); this ensures empty-string CSRF tokens are
treated as valid values and remain consistent with mergeApolloHeadersWithCsrf
which may set X-CSRFToken to ''.

---

Nitpick comments:
In `@frontend/__tests__/unit/server/apolloCookieHeader.test.ts`:
- Around line 3-71: Missing unit test for csrfToken = '': add a test case to
ensure mergeCookieHeader and mergeApolloHeadersWithCsrf treat an empty-string
CSRF token like null (preserve existing cookies including any stale csrftoken
and set X-CSRFToken to ''). Add a new test similar to the "preserves incoming
cookies when csrf token is missing" case but pass '' as the csrf argument,
assert that the Cookie header remains unchanged (including any existing
csrftoken) and that headers['X-CSRFToken'] === '' and that no duplicate
csrftoken entries are created (use the existing mergeCookieHeader helper in the
test to validate deduplication).

In `@frontend/src/server/apolloClient.ts`:
- Around line 9-10: Both getCsrfTokenOnServer and getRequestCookieHeaderOnServer
individually call await cookies(), causing two redundant cookies() reads per
request in the Apollo setContext path; fix by reading the cookie store once
inside the setContext callback (call cookies() once), then pass that single
cookie store into both helpers (either by adding an optional parameter to
getCsrfTokenOnServer/getRequestCookieHeaderOnServer or by creating an internal
helper that accepts the cookie store) so the exported helpers remain unchanged
for external callers but the hot path re-uses the same cookie store.
- Around line 52-59: The helper getRequestCookieHeaderOnServer is exported but
only used internally by createApolloClient; remove the unnecessary export to
shrink the public API surface by changing getRequestCookieHeaderOnServer to a
module-private function (e.g., drop the export keyword or convert it to a
non-exported const/async function) and keep its call sites (createApolloClient)
unchanged.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 18, 2026
@Shofikul-Isl4m
Copy link
Contributor

you have to address sonarcloud issues
Screenshot from 2026-02-21 13-07-43

@xDipzz
Copy link
Author

xDipzz commented Feb 21, 2026

you have to address sonarcloud issues Screenshot from 2026-02-21 13-07-43

Thanks ,SonarCloud shows Quality Gate passed with 1 new issue. I’ll review it and address it.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 21, 2026
@xDipzz
Copy link
Author

xDipzz commented Feb 21, 2026

@arkid15r @kasya Please review PR #3985 and let me know if any changes are required.

@preeeetham
Copy link
Contributor

Good fix — the root cause was correctly identified. The old code was setting Cookie: csrftoken=... which nuked all existing session cookies during SSR, and the new approach of reading all cookies from next/headers and merging them properly makes sense. The deduplication logic in mergeCookieHeader is clean and the case-insensitive header handling is a nice touch.

A couple things I'd flag though: getRequestCookieHeaderOnServer forwards every cookie the client sent to the backend via cookieStore.getAll(). That includes analytics cookies, third-party cookies, anything the browser attached. Ideally only the relevant auth cookies like sessionid and csrftoken should be forwarded — forwarding everything violates least-privilege and could hit header size limits if there are many cookies. A simple whitelist filter before joining would tighten this up.

On the cookie merging side, mergeCookieHeader concatenates values directly with split(';') and string joining without any encoding or sanitization. Since the values come from cookies().getAll() which returns already-parsed cookies this is probably fine in practice, but if a cookie value ever contained a semicolon it would break the parsing. Not a likely scenario but worth being aware of. Also, an empty-string CSRF token currently gets treated as a valid value and produces csrftoken= in the header — might want to treat that the same as null and skip setting CSRF headers entirely, since an empty token is almost certainly a bug upstream.

@xDipzz
Copy link
Author

xDipzz commented Feb 28, 2026

Good fix — the root cause was correctly identified. The old code was setting Cookie: csrftoken=... which nuked all existing session cookies during SSR, and the new approach of reading all cookies from next/headers and merging them properly makes sense. The deduplication logic in mergeCookieHeader is clean and the case-insensitive header handling is a nice touch.

A couple things I'd flag though: getRequestCookieHeaderOnServer forwards every cookie the client sent to the backend via cookieStore.getAll(). That includes analytics cookies, third-party cookies, anything the browser attached. Ideally only the relevant auth cookies like sessionid and csrftoken should be forwarded — forwarding everything violates least-privilege and could hit header size limits if there are many cookies. A simple whitelist filter before joining would tighten this up.

On the cookie merging side, mergeCookieHeader concatenates values directly with split(';') and string joining without any encoding or sanitization. Since the values come from cookies().getAll() which returns already-parsed cookies this is probably fine in practice, but if a cookie value ever contained a semicolon it would break the parsing. Not a likely scenario but worth being aware of. Also, an empty-string CSRF token currently gets treated as a valid value and produces csrftoken= in the header — might want to treat that the same as null and skip setting CSRF headers entirely, since an empty token is almost certainly a bug upstream.

Thanks for the review. I agree on least-privilege and header-size concerns, but I didn’t
want to guess a cookie allowlist and risk breaking SSR auth/session flows. If maintainers
confirm the expected cookie set to forward, I can follow up with a small allowlist + tests;
for now this PR keeps SSR behavior consistent by preserving the incoming request cookies.

Copy link
Collaborator

@arkid15r arkid15r left a comment

Choose a reason for hiding this comment

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

Please resolve conflicts

@xDipzz
Copy link
Author

xDipzz commented Mar 1, 2026

Please resolve conflicts

@arkid15r Conflicts have been resolved and the branch is up to date with main. Please let me know if any further changes are required.

@xDipzz xDipzz requested a review from arkid15r March 1, 2026 13:52
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 8, 2026

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.

Bug: SSR Apollo client overwrites Cookie header and drops auth/session cookies

4 participants