Skip to content

Security: Client-Supplied previousResponseId Not Bound to Authenticated User — Cross-User Conversation Access #1070

@BenjaminMichaelis

Description

@BenjaminMichaelis

Summary

ChatController accepts a previousResponseId from the client and forwards it directly to Azure OpenAI without verifying that the ID was issued to the currently authenticated user. Since all users share the same Azure OpenAI deployment and API key, any authenticated user who learns another user's response ID can continue (and read back) that user's conversation.

Affected Code

EssentialCSharp.Web/Controllers/ChatController.cs:

var previousResponseId = string.IsNullOrWhiteSpace(request.PreviousResponseId)
    ? null
    : request.PreviousResponseId.Trim();   // ← taken from client, no ownership check

var (response, responseId) = await _AiChatService.GetChatCompletion(
    prompt: request.Message,
    previousResponseId: previousResponseId,  // ← forwarded to OpenAI as-is
    ...);

AIChatService.cs passes it directly to ResponseCreationOptions.PreviousResponseId.

Attack Scenario

  1. Attacker authenticates and sends a normal chat request.
  2. Attacker learns a victim's responseId (e.g., via shared browser, logs, XSS, or by observing network traffic).
  3. Attacker calls POST /api/chat/message with PreviousResponseId set to the victim's ID.
  4. Azure OpenAI resumes the victim's conversation context, potentially exposing prior conversation content in the new response.

Risk

OWASP AI Agent Security — §3 Memory & Context Security / §8 Data Protection & Privacy

  • Azure OpenAI response IDs are opaque but not bound to a specific API caller identity; they're valid for any caller using the same deployment.
  • The lack of server-side binding means there's no isolation between user conversation sessions at the application layer.
  • The lastResponseId is also persisted in localStorage client-side, widening exposure.

Recommended Mitigation

Store issued response IDs server-side keyed to the authenticated user (e.g., in the ASP.NET session or a cache), and validate on each request:

// On chat completion, store the response ID against the user
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
_cache.Set($"chat:lastResponseId:{userId}", responseId, TimeSpan.FromHours(24));

// On next request, validate the supplied ID matches what's stored for this user
if (previousResponseId != null)
{
    var storedId = _cache.Get<string>($"chat:lastResponseId:{userId}");
    if (storedId != previousResponseId)
        return BadRequest(new { error = "Invalid conversation context." });
}

Alternatively, enforce that the server always issues and tracks the response ID server-side, never trusting the client-supplied value.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions