Skip to content

security: move session secrets to HttpOnly cookies#404

Open
Namraa310806 wants to merge 8 commits into
FireFistisDead:masterfrom
Namraa310806:fix/session-secret-cookie-storage
Open

security: move session secrets to HttpOnly cookies#404
Namraa310806 wants to merge 8 commits into
FireFistisDead:masterfrom
Namraa310806:fix/session-secret-cookie-storage

Conversation

@Namraa310806
Copy link
Copy Markdown
Contributor

@Namraa310806 Namraa310806 commented May 31, 2026

Summary

This PR reduces exposure of session credentials by removing session_secret from API responses and moving secret handling to secure HttpOnly cookies.

Previously, session secrets were returned in upload/session bootstrap responses and could be accessed by browser JavaScript, client-side logs, analytics tooling, browser extensions, or other frontend integrations.

This change keeps session authentication functional while significantly reducing the amount of secret material exposed to the browser environment.

Changes Made

  • Added secure session secret cookie handling on the backend.

  • Introduced helper utilities for:

    • session secret cookie creation
    • cookie lookup and resolution
    • request/session secret normalization
  • Stored session secrets in HttpOnly cookies instead of exposing them through API responses.

  • Updated upload and session-extension flows to use cookie-based credential handling.

  • Removed reliance on API responses returning session_secret.

  • Updated frontend API clients to send credentials using:

    • axios.defaults.withCredentials = true
    • credentials: "include" for streaming requests
  • Moved session tracking to sessionStorage and reduced browser persistence of sensitive data.

  • Added migration support for legacy session storage.

  • Added regression tests verifying:

    • session_secret is not returned in upload responses
    • session cookies are issued correctly
    • session identifiers continue to function
    • historical session migration behavior remains intact

Related issue

Fixes: #359

Testing

  • I ran the relevant checks locally
  • I verified the app still starts
  • I tested the affected flow end-to-end

Verified Scenarios

  • New PDF uploads continue to work.
  • Existing sessions can still be extended.
  • Session cookies are issued correctly.
  • Upload responses no longer expose session_secret.
  • Frontend requests continue to authenticate successfully.
  • Streaming requests continue to function correctly.
  • Historical session migration continues to work.
  • Browser storage no longer persists sensitive session secrets unnecessarily.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas

Screenshots / recordings

N/A – Backend and security-focused change.

Notes

This change improves credential handling by reducing exposure of bearer-style session secrets to browser-accessible contexts.

The implementation preserves existing application behavior while moving sensitive session credentials into HttpOnly cookies that are inaccessible to frontend JavaScript.

No database migrations or infrastructure changes are required.

Security

  • No sensitive data included

Summary by CodeRabbit

  • New Features

    • Sessions now support identifiers without requiring secrets
    • Automatic migration of legacy session data to secure session storage
    • Server-side encrypted cookie-based session authentication
  • Bug Fixes

    • Improved credential and authentication cookie handling across all requests
    • Enhanced session persistence during PDF uploads and inference queries

Copilot AI review requested due to automatic review settings May 31, 2026 04:46
@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

@Namraa310806 is attempting to deploy a commit to the firefistisdead's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 31, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR secures session credential handling by moving session_secret from browser-accessible responses to server-managed HttpOnly cookies. The backend encrypts and stores secrets server-side, generating per-session tokens; all endpoints resolve secrets from request bodies or cookies before injecting into validations. The frontend enables credentialed requests, stores session identifiers (with optional null secrets) in sessionStorage, migrates legacy localStorage data, and upserts sessions on upload using fallback secret logic. Tests validate cookie-based flows and nullable storage.

Changes

Session Secret Cookie-Based Authorization

Layer / File(s) Summary
Server cookie infrastructure and CORS
server.js
CORS enables credentialed requests; per-session cookie naming, server-side encrypted token→secret storage (in-memory + optional Redis), AES-256-GCM encrypt/decrypt, cookie parsing, body-first resolution with cookie fallback, HttpOnly secure cookie setter, and session secret attachment helper.
Upload and process-from-url endpoint updates
server.js
/upload resolves secret, sets cookie, omits from response; /process-from-url resolves secret, forwards with session_id only when both present, sets cookie, omits from response.
Inference endpoints: /ask, /ask/stream, /summarize, /knowledge-gaps
server.js
Each endpoint resolves session_secret from cookie/body and injects into Zod payload; /ask/stream fetch includes credentials.
Sessions lookup preprocessing
server.js
/sessions/lookup resolves and attaches session_secret per session before Zod validation.
Frontend credential request enablement
frontend/src/services/api.js
Axios global withCredentials default; streaming fetch includes credentials: "include".
Frontend session storage, migration, and upload integration
frontend/src/App.js
Comments updated for sessionStorage identifiers + HttpOnly secrets; loadKnownSessions/upsertKnownSession allow nullable session_secret and trim session_id; one-time localStoragesessionStorage migration parsing JSON/base64, mapping blank secrets to null, merging/deduplicating, capping to 50; PDF upload upserts session and record with fallback (returned or prior or null).
Frontend tests: session storage and migration
frontend/src/App.test.js
Issue #234 header updated; test helpers adjusted to accept nullable sessionSecret, migrate blank secrets as null, deduplicate by session_id; test cases expect retained entries with session_secret: null.
Backend tests: cookie assertions and fallback validation
server.test.js
Upload response tests assert session_secret omitted and pdfqa_session_secret_ cookie set; new POST /ask regression test verifies cookie-derived secret forwarding via Cookie header.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #234 — Main changes implement the proposed credential storage redesign, directly modifying App.js session functions and adding server-side cookie-based session_secret handling per the issue's objectives.
  • #359 — This PR directly addresses the security issue by removing session_secret from frontend-accessible JSON responses and storing it server-side in HttpOnly cookies, reducing XSS and browser extension exfiltration risks.

Possibly related PRs

  • FireFistisDead/pdf-qa-bot#209 — Main PR adjusts the session-secret enforcement model introduced by this PR on the same App.js and server.js session-credential functions.
  • FireFistisDead/pdf-qa-bot#251 — Main PR updates loadKnownSessions and upsertKnownSession functions introduced in this PR, changing how blank/missing session_secret is handled.
  • FireFistisDead/pdf-qa-bot#175 — Main PR's /upload and /process-from-url session-integrity validation directly intersects with this PR's session extension and authorization logic.

Suggested labels

quality:exceptional, mentor:FireFistisDead

Suggested reviewers

  • FireFistisDead

Poem

🐰 A cookie-shaped quest to keep secrets safe,
No more JavaScript exposure to face,
HttpOnly tokens hide in the vault,
While sessions migrate without a fault,
Frontend and backend in harmony bloom! 🍪✨

🚥 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 clearly and concisely summarizes the main security change: moving session secrets from API responses to HttpOnly cookies, which is the primary focus of this PR.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, related issues, testing verification, checklists, and security notes. All template sections are addressed.
Linked Issues check ✅ Passed The PR fully addresses issue #359 by implementing HttpOnly cookie-based session secret storage, removing session_secret from API responses for /upload and /process-from-url, and reducing frontend exposure of capability tokens as required.
Out of Scope Changes check ✅ Passed All changes are scoped to the security objective: session credential handling improvements, cookie-based storage, CORS credential support, API response modifications, and supporting frontend/test updates are all directly related to reducing session_secret exposure.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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

@github-actions github-actions Bot added backend Express or API gateway work docs Documentation only enhancement New feature or request feature A new feature or improvement fix A targeted fix or cleanup frontend Frontend-related work level:advanced rag-service FastAPI / model service work type:security type:testing labels May 31, 2026
Comment thread server.js Fixed
Copy link
Copy Markdown
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.

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/App.test.js (1)

48-70: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Test helper loadKnownSessions is inconsistent with App.js implementation.

The loadKnownSessions helper in this test file still filters out entries with missing or blank session_secret (lines 60-61), but the App.js implementation at lines 119-132 no longer requires session_secret. This causes downstream test failures:

  1. Test at lines 175-182 expects sessions with null secrets to be retained, but loadKnownSessions filters them out.
  2. Test at lines 199-213 expects 3 sessions, but the helper filters to only 1.
🐛 Proposed fix: Update the test helper to match App.js
 function loadKnownSessions() {
   try {
     const raw = sessionStorage.getItem(SESSION_KEY);
     if (!raw) return [];
     const parsed = decodePayload(raw);
     if (!Array.isArray(parsed)) return [];
     return parsed
       .filter(
         (s) =>
           s &&
           typeof s.session_id === "string" &&
-          s.session_id.trim() !== "" &&
-          typeof s.session_secret === "string" &&
-          s.session_secret.trim() !== "",
+          s.session_id.trim() !== "",
       )
       .map((s) => ({
         session_id: s.session_id.trim(),
-        session_secret: s.session_secret.trim(),
+        session_secret:
+          typeof s.session_secret === "string" && s.session_secret.trim() !== ""
+            ? s.session_secret.trim()
+            : null,
       }));
   } catch (_) {
     return [];
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/App.test.js` around lines 48 - 70, The test helper
loadKnownSessions is too strict compared to App.js: remove the checks that
require a non-blank session_secret and instead only validate session_id (string
and non-empty). Update the filter in loadKnownSessions to only ensure s &&
typeof s.session_id === "string" && s.session_id.trim() !== ""; in the map keep
session_id as s.session_id.trim() and preserve session_secret as-is (e.g.,
s.session_secret or null) — if session_secret is a string you may trim it,
otherwise allow null/undefined to match App.js behavior; keep SESSION_KEY and
decodePayload usage and existing try/catch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/App.test.js`:
- Around line 48-70: The test helper loadKnownSessions is too strict compared to
App.js: remove the checks that require a non-blank session_secret and instead
only validate session_id (string and non-empty). Update the filter in
loadKnownSessions to only ensure s && typeof s.session_id === "string" &&
s.session_id.trim() !== ""; in the map keep session_id as s.session_id.trim()
and preserve session_secret as-is (e.g., s.session_secret or null) — if
session_secret is a string you may trim it, otherwise allow null/undefined to
match App.js behavior; keep SESSION_KEY and decodePayload usage and existing
try/catch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fd98ab7c-932b-4958-ac2d-fad12b6b2382

📥 Commits

Reviewing files that changed from the base of the PR and between 04e78f5 and 22d20fb.

📒 Files selected for processing (5)
  • frontend/src/App.js
  • frontend/src/App.test.js
  • frontend/src/services/api.js
  • server.js
  • server.test.js

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Comment thread server.js Fixed
@github-actions github-actions Bot added the bug Something isn't working label May 31, 2026
Copilot AI review requested due to automatic review settings May 31, 2026 19:33
Copy link
Copy Markdown
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server.js`:
- Around line 504-505: SESSION_SECRET_MAP is an in-memory Map used to resolve
session cookie tokens (with SESSION_SECRET_TTL_MS controlling TTL), which causes
sessions to break across process restarts or different instances; replace the
Map with a shared backing store (e.g., Redis) and persist token -> { encrypted,
expiry } with an expiry set from SESSION_SECRET_TTL_MS, update all reads/writes
that reference SESSION_SECRET_MAP to async Redis GET/SET/EXPIRE operations (or
use a stateless signed cookie approach instead: embed and verify the
session_secret in the cookie), keep an in-memory fallback only for
single-process dev, and add robust error handling so lookups fall back
gracefully when the shared store is unavailable.
- Around line 566-568: The per-token eviction timer created in the
SESSION_SECRET_MAP cleanup uses setTimeout(...) which can keep the Node process
alive; update the timer logic around SESSION_SECRET_MAP.delete(token) to store
the return value from setTimeout, call .unref() on that timer when available,
and then let it fire normally after SESSION_SECRET_TTL_MS + 1000 so the timer no
longer holds the event loop open; reference the token variable, the
SESSION_SECRET_MAP deletion, and SESSION_SECRET_TTL_MS when applying this
change.
🪄 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 Plus

Run ID: 08a46a1d-4bc6-4910-a552-ded2aa11a54a

📥 Commits

Reviewing files that changed from the base of the PR and between 22d20fb and 57bb115.

📒 Files selected for processing (2)
  • frontend/src/App.js
  • server.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/App.js

Comment thread server.js Outdated
Comment thread server.js Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings June 1, 2026 18:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

frontend/src/App.js:154

  • upsertKnownSession is still writing session data to localStorage, which contradicts the updated comment/migration and reintroduces the exact storage behavior the tests are guarding against. This should write to sessionStorage (and keep the same try/catch), otherwise the new migration will remove the legacy key but subsequent upserts will immediately recreate it.
        { session_id: normalizedSessionId, session_secret: normalizedSessionSecret },
        ...existing.filter((s) => s.session_id !== normalizedSessionId),
      ];
      try {
        localStorage.setItem(SESSION_STORAGE_KEY, encodePayload(next.slice(0, 50)));
      } catch (_) {

Comment thread server.js
Comment on lines +795 to +801
res.cookie(getSessionSecretCookieName(sessionId), token, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: SESSION_SECRET_TTL_MS,
});
Comment thread server.js
Comment on lines +702 to +708
SESSION_SECRET_MEMORY_MAP.set(token, { encrypted, expiry });
const timeout = setTimeout(() => {
SESSION_SECRET_MEMORY_MAP.delete(token);
}, SESSION_SECRET_TTL_MS + 1000);
if (typeof timeout.unref === "function") {
timeout.unref();
}
Comment thread server.js
Comment on lines +593 to +598
// Encryption key must be provided via env var as base64-encoded 32 bytes.
// If not present, generate a runtime-only key (lost on restart) and log a warning.
let ENC_KEY = null;
const _initEncKey = () => {
if (ENC_KEY) return;
const fromEnv = (process.env.SESSION_SECRET_ENC_KEY || "").trim();
Comment thread server.js Fixed
Copilot AI review requested due to automatic review settings June 1, 2026 18:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread server.js
Comment on lines +752 to +772
return header.split(";").reduce((cookies, pair) => {
const separatorIndex = pair.indexOf("=");
if (separatorIndex === -1) {
return cookies;
}

const rawName = pair.slice(0, separatorIndex).trim();
const rawValue = pair.slice(separatorIndex + 1).trim();

if (!rawName) {
return cookies;
}

try {
cookies[rawName] = decodeURIComponent(rawValue);
} catch (_) {
cookies[rawName] = rawValue;
}

return cookies;
}, {});
Comment thread server.js
Comment on lines +710 to +725
if (sessionSecretRedisClient) {
await _storeSessionSecretInRedis(token, encrypted, expiry);
}

SESSION_SECRET_MEMORY_MAP.set(token, { encrypted, expiry });
// Only create a per-token timer for in-memory fallback. When Redis is
// configured we rely on Redis TTLs and lazy eviction to avoid creating
// large numbers of active timers in-process.
if (!sessionSecretRedisClient) {
const timeout = setTimeout(() => {
SESSION_SECRET_MEMORY_MAP.delete(token);
}, SESSION_SECRET_TTL_MS + 1000);
if (typeof timeout.unref === "function") {
timeout.unref();
}
}
Comment thread server.js
Comment on lines +815 to +823
// Respect operator-configured SameSite. If operators explicitly request
// 'none', ensure the Secure flag is set (browsers require this for None).
const sameSiteRaw = (SESSION_SECRET_COOKIE_SAMESITE || "lax").toString();
const sameSite = ("" + sameSiteRaw).toLowerCase();
const secureFlag = process.env.NODE_ENV === "production" || sameSite === "none";

if (sameSite === "none" && !secureFlag) {
console.warn("SESSION_SECRET_COOKIE_SAMESITE=none was requested but secure cookies are not enabled; forcing Secure flag to true for compatibility.");
}
Comment thread frontend/src/App.js
Comment on lines +149 to 153
{ session_id: normalizedSessionId, session_secret: normalizedSessionSecret },
...existing.filter((s) => s.session_id !== normalizedSessionId),
];
try {
localStorage.setItem(SESSION_STORAGE_KEY, encodePayload(next.slice(0, 50)));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Express or API gateway work bug Something isn't working docs Documentation only enhancement New feature or request feature A new feature or improvement fix A targeted fix or cleanup frontend Frontend-related work level:advanced rag-service FastAPI / model service work type:security type:testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security]: Reduce frontend exposure of session_secret capability token after upload

3 participants