Skip to content

Session IP Binding

Priority: 🔵 LONG-TERM Estimated Time: 3 hours Risk Level: MEDIUM Impact: Prevent session hijacking attacks

← Back to Enhancements


Issue Description

Current session management:

  • ✅ Uses cryptographically secure UUIDs
  • ✅ Redis-backed TTL expiration
  • ❌ No IP address validation
  • ❌ Session can be used from any IP

Risk: If session ID is stolen, attacker can use it from anywhere.


Proposed Solution

Bind sessions to client IP address:

  • Store client IP in session metadata
  • Validate IP on each request
  • Optional: Allow IP changes with re-authentication
  • Configurable for proxy/load balancer setups

Implementation

Note: Redis is included in the local development environment when using ./scripts/local (part of the Docker stack).

Update Session Metadata

Update src/services/session-store.ts:

typescript
export interface SessionMetadata {
  sessionId: string;
  createdAt: number;
  lastAccessedAt: number;
  userId?: string;
  clientIp?: string; // NEW
  userAgent?: string; // NEW
  fingerprint?: string; // NEW: Hash of IP + User-Agent
}

Transport Creation

Update src/mcp/mcp.ts:

typescript
export async function createTransport(req: Request): Promise<StreamableHTTPServerTransport> {
  const sessionStore = getSessionStore();
  const clientIp = req.ip;
  const userAgent = req.headers["user-agent"];

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => randomUUID(),
    enableJsonResponse: true,
    onsessioninitialized: async (sessionId: string) => {
      transports[sessionId] = transport;

      const now = Date.now();
      await sessionStore.set(sessionId, {
        createdAt: now,
        lastAccessedAt: now,
        clientIp, // NEW
        userAgent, // NEW
        fingerprint: createFingerprint(clientIp, userAgent), // NEW
      });

      mcpSessionsActive.inc();
      mcpSessionsTotal.inc({ status: "created" });

      logger.info("Session created", { sessionId, clientIp });
    },
  });

  const server = createMcpServer();
  await server.connect(transport);

  return transport;
}

function createFingerprint(ip?: string, userAgent?: string): string {
  const data = `${ip}:${userAgent}`;
  return crypto.createHash("sha256").update(data).digest("hex");
}

Request Validation

Update src/routes/mcp.ts:

typescript
mcpRouter.post("/", async (req: Request, res: Response): Promise<void> => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (sessionId) {
    const transport = await getTransport(sessionId);
    if (transport) {
      // Validate IP binding
      const sessionStore = getSessionStore();
      const metadata = await sessionStore.get(sessionId);

      if (metadata && config.session.ipBinding.enabled) {
        const currentIp = req.ip;
        const currentFingerprint = createFingerprint(
          currentIp,
          req.headers["user-agent"]
        );

        if (metadata.fingerprint !== currentFingerprint) {
          logger.warn("Session fingerprint mismatch", {
            sessionId,
            originalIp: metadata.clientIp,
            currentIp,
          });

          if (config.session.ipBinding.strict) {
            // Reject the request
            await removeTransport(sessionId);

            res.status(403).json({
              jsonrpc: "2.0",
              error: {
                code: -32001,
                message: "Forbidden",
                data: {
                  reason: "session_fingerprint_mismatch",
                  details: "Session cannot be used from this location",
                },
              },
              id: null,
            });
            return;
          } else {
            // Warn but allow (for mobile/proxy scenarios)
            logger.info("Allowing session with IP mismatch (non-strict mode)", {
              sessionId,
            });
          }
        }
      }

      await sessionStore.touch(sessionId);
      await transport.handleRequest(req, res, req.body);
      return;
    }
  }

  // ... rest of handler
});

Configuration

Add configuration in src/config/session.ts:

typescript
export const sessionConfig = {
  ttlSeconds: parseInt(process.env.MCP_SESSION_TTL_SECONDS ?? "86400", 10),
  keyPrefix: process.env.MCP_SESSION_KEY_PREFIX ?? "mcp:session:",

  // IP binding configuration
  ipBinding: {
    enabled: process.env.SESSION_IP_BINDING_ENABLED !== "false",
    strict: process.env.SESSION_IP_BINDING_STRICT === "true", // false = warn only
  },
} as const;

Configuration Options

Add to .env.example:

env
# Session IP binding
SESSION_IP_BINDING_ENABLED=true
SESSION_IP_BINDING_STRICT=false  # true = reject, false = warn

Testing

Create tests in src/routes/mcp.test.ts:

typescript
describe("Session IP Binding", () => {
  it("should allow requests from same IP", async () => {
    // Test
  });

  it("should reject requests from different IP in strict mode", async () => {
    // Test
  });

  it("should warn but allow in non-strict mode", async () => {
    // Test
  });

  it("should handle proxy headers correctly", async () => {
    // Test X-Forwarded-For
  });
});

Considerations

Pros

  • Prevents session hijacking
  • Additional layer of security
  • Configurable strictness

Cons

  • Mobile users change IPs frequently
  • VPNs/proxies cause issues
  • Corporate networks with multiple egress IPs

Recommendation

Use non-strict mode by default, strict mode for high-security deployments.


Deployment Scenarios

High Security Environment

env
SESSION_IP_BINDING_ENABLED=true
SESSION_IP_BINDING_STRICT=true

Best for: Internal tools, desktop-only applications, controlled network environments.

Mobile/Flexible Environment

env
SESSION_IP_BINDING_ENABLED=true
SESSION_IP_BINDING_STRICT=false

Best for: Mobile apps, users on VPNs, corporate networks with load balancers.

Development

env
SESSION_IP_BINDING_ENABLED=false

Best for: Local development, testing.


Monitoring

Add metrics for IP binding violations:

typescript
export const sessionIpMismatch = new promClient.Counter({
  name: "session_ip_mismatch_total",
  help: "Total session IP mismatches detected",
  labelNames: ["strict_mode", "action"],
});

Set up alerts for suspicious activity:

promql
rate(session_ip_mismatch_total{action="rejected"}[5m]) > 10


← Back to Enhancements

Released under the MIT License.