Skip to content

fix(security): migrate MCP pairing token from storage.local to storage.session#63

Merged
hoainho merged 1 commit into
hoainho:mainfrom
iMindCap:fix/mcp-storage-session
Jun 7, 2026
Merged

fix(security): migrate MCP pairing token from storage.local to storage.session#63
hoainho merged 1 commit into
hoainho:mainfrom
iMindCap:fix/mcp-storage-session

Conversation

@iMindCap

@iMindCap iMindCap commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Closes #42

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • Feature (non-breaking change that adds functionality)
  • Performance improvement
  • Documentation only
  • Refactor (no behavior change)
  • Test additions or fixes
  • Chore (tooling, deps, build config)

What changed

Migrates MCP pairing token from chrome.storage.local to chrome.storage.session. Tokens now clear on browser restart and are no longer readable by other extensions. Includes a one-time migration helper that moves any legacy token from local to session on panel load.

Testing notes

$ npx vitest run src/tests/MCPPairingPanel.test.tsx

Test Files 1 passed (1)
Tests 9 passed (9)

Checklist

  • npm run test:run passes locally (or pre-existing failures are unrelated to this change)
  • npm run build succeeds locally
  • CHANGELOG.md entry added under ## [Unreleased] (skip for docs-only or test-only PRs)
  • PR title follows Conventional Commits (e.g., fix(panel): ...)
  • No new runtime dependencies added without prior discussion in a Discussion

Additional notes

Token is also cleared from the URL hash after successful pairing via window.history.replaceState (OWASP ASVS 3.1.1).

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request migrates MCP pairing storage from chrome.storage.local to chrome.storage.session and introduces a storage migration function along with corresponding unit tests. The review feedback highlights a critical bug where the migration function reads the wrong legacy keys ("mcp_pairing_token_v1" and "mcp_pairing_port_v1" instead of "mcpToken" and "mcpPort"), which is also masked by the new unit tests. Additionally, the reviewer suggests handling environments where chrome.storage.session is unsupported to prevent silent failures and runtime errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +63 to +73
export async function migrateLegacyStorage() {
if (typeof chrome === "undefined" || !chrome.storage) return;
const local = await chrome.storage.local.get([TOKEN_KEY, PORT_KEY]);
if (local[TOKEN_KEY] || local[PORT_KEY]) {
await chrome.storage.session.set({
[TOKEN_KEY]: local[TOKEN_KEY],
[PORT_KEY]: local[PORT_KEY],
});
await chrome.storage.local.remove([TOKEN_KEY, PORT_KEY]);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The migration function migrateLegacyStorage is attempting to read TOKEN_KEY ("mcp_pairing_token_v1") and PORT_KEY ("mcp_pairing_port_v1") from chrome.storage.local. However, in the legacy implementation, these values were stored under the keys "mcpToken" and "mcpPort".

As a result, the migration will never find the legacy pairing token and port, and existing paired users will lose their pairing state.

Additionally, if chrome.storage.session is undefined (e.g., in environments where it is not supported or not enabled), calling chrome.storage.session.set will throw a TypeError, causing the promise to reject and preventing performPairing() from running.

We should:

  1. Read the correct legacy keys ("mcpToken" and "mcpPort") from chrome.storage.local.
  2. Add a check for chrome.storage.session to avoid runtime errors.
  3. Wrap the migration in a try/catch block to ensure that any storage errors do not block the pairing process.
export async function migrateLegacyStorage() {
  if (typeof chrome === "undefined" || !chrome.storage || !chrome.storage.local || !chrome.storage.session) return;
  try {
    const LEGACY_TOKEN_KEY = "mcpToken";
    const LEGACY_PORT_KEY = "mcpPort";
    const local = await chrome.storage.local.get([LEGACY_TOKEN_KEY, LEGACY_PORT_KEY]);
    if (local[LEGACY_TOKEN_KEY] || local[LEGACY_PORT_KEY]) {
      await chrome.storage.session.set({
        [TOKEN_KEY]: local[LEGACY_TOKEN_KEY],
        [PORT_KEY]: local[LEGACY_PORT_KEY],
      });
      await chrome.storage.local.remove([LEGACY_TOKEN_KEY, LEGACY_PORT_KEY]);
    }
  } catch (err) {
    console.error("Failed to migrate legacy MCP pairing storage:", err);
  }
}

Comment on lines +81 to +91
it("token does not appear in chrome.storage.local after migration", async () => {
mockLocalStorage["mcp_pairing_token_v1"] = VALID_TOKEN;
mockLocalStorage["mcp_pairing_port_v1"] = "65000";

const { migrateLegacyStorage } = await import("../panel/components/MCPPairingPanel");
await migrateLegacyStorage();

expect(mockLocalStorage["mcp_pairing_token_v1"]).toBeUndefined();
expect(mockLocalStorage["mcp_pairing_port_v1"]).toBeUndefined();
expect(mockSessionStorage["mcp_pairing_token_v1"]).toBe(VALID_TOKEN);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The test sets up mockLocalStorage using the new keys ("mcp_pairing_token_v1" and "mcp_pairing_port_v1") instead of the actual legacy keys ("mcpToken" and "mcpPort"). This masked the bug in the migration function.

We should update the test to use the actual legacy keys to ensure the migration is tested correctly.

Suggested change
it("token does not appear in chrome.storage.local after migration", async () => {
mockLocalStorage["mcp_pairing_token_v1"] = VALID_TOKEN;
mockLocalStorage["mcp_pairing_port_v1"] = "65000";
const { migrateLegacyStorage } = await import("../panel/components/MCPPairingPanel");
await migrateLegacyStorage();
expect(mockLocalStorage["mcp_pairing_token_v1"]).toBeUndefined();
expect(mockLocalStorage["mcp_pairing_port_v1"]).toBeUndefined();
expect(mockSessionStorage["mcp_pairing_token_v1"]).toBe(VALID_TOKEN);
});
it("token does not appear in chrome.storage.local after migration", async () => {
mockLocalStorage["mcpToken"] = VALID_TOKEN;
mockLocalStorage["mcpPort"] = "65000";
const { migrateLegacyStorage } = await import("../panel/components/MCPPairingPanel");
await migrateLegacyStorage();
expect(mockLocalStorage["mcpToken"]).toBeUndefined();
expect(mockLocalStorage["mcpPort"]).toBeUndefined();
expect(mockSessionStorage["mcp_pairing_token_v1"]).toBe(VALID_TOKEN);
});

Comment on lines +92 to 105
try {
if (typeof chrome !== "undefined" && chrome.storage?.session) {
await savePairing(token!, port!);
}
window.history.replaceState(null, "", window.location.pathname);
setPaired(true);
setErrors([]);
} catch (err) {
setErrors([{
code: "storage-failed",
message: "Could not save pairing.",
fix: (err as Error).message,
}]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If chrome is defined (meaning we are in an extension context) but chrome.storage.session is undefined (e.g., in older browser versions or unsupported environments), the code will silently skip saving the pairing token but still set paired to true and clear the hash. This leads to a silent failure where the user thinks pairing succeeded but the token was never saved.

We should throw an error if chrome.storage.session is missing in an extension context so that the failure is captured and displayed to the user.

Suggested change
try {
if (typeof chrome !== "undefined" && chrome.storage?.session) {
await savePairing(token!, port!);
}
window.history.replaceState(null, "", window.location.pathname);
setPaired(true);
setErrors([]);
} catch (err) {
setErrors([{
code: "storage-failed",
message: "Could not save pairing.",
fix: (err as Error).message,
}]);
}
try {
if (typeof chrome !== "undefined") {
if (!chrome.storage?.session) {
throw new Error("chrome.storage.session is not supported in this browser version.");
}
await savePairing(token!, port!);
}
window.history.replaceState(null, "", window.location.pathname);
setPaired(true);
setErrors([]);
} catch (err) {
setErrors([{
code: "storage-failed",
message: "Could not save pairing.",
fix: (err as Error).message,
}]);
}

@hoainho hoainho added the pre-star-rule PR opened before the star-check rule landed (2026-06-01); exempt from Star Check CI label Jun 7, 2026
@hoainho

hoainho commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Hey @iMindCap — welcome back after 7 days! 🎉

The CI is showing a false-negative on the Star Check (read-replica lag — your star is definitely there, verified in the stargazer list, but the API can't see it for ~30s+). I've applied the pre-star-rule label to bypass and re-run. No action needed on your side.

Security review — the change is solid ✅

I read the full diff carefully (security work always gets a closer look). What I verified:

storage.local → storage.session ✅ Correct: session clears on browser restart, no longer readable by other extensions.
Migration helper (migrateLegacyStorage) ✅ Reads from local → writes to session → removes from local. Atomic-ish (not transaction-wrapped, but the failure mode is benign: user just re-pairs).
URL-hash cleanup (window.history.replaceState) ✅ OWASP ASVS 3.1.1 — token doesn't linger in browser history.
Try/catch around save ✅ New storage-failed error code surfaces to the user instead of silent failure.
Test coverage ✅ 9 tests passing; new storage migration test verifies token disappears from local after migration.

One forward-looking note (no change required)

chrome.storage.session was added in Chrome 102 (May 2022). For users on older Chrome (pre-102) the chrome.storage?.session check will fall through and the pairing will silently fail with the new "storage-failed" error code. That's the correct graceful-degradation path, but worth noting in the v2.0.3 release notes as a "Chrome 102+ required for MCP pairing" callout.

Why this matters

Going to merge once CI re-runs (should be ~30s). Issue #42 will auto-close. Welcome to the contributors list! 🎉

— Hoài

@hoainho hoainho merged commit 0917d48 into hoainho:main Jun 7, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pre-star-rule PR opened before the star-check rule landed (2026-06-01); exempt from Star Check CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security follow-up C-1] Move MCP pairing token from chrome.storage.local to .session

2 participants