SciSharp / SciSharp/BotSharp

Missing Conversation Ownership Check on SQL Driver Endpoint Allows Any Authenticated User to Execute Arbitrary SQL

Open
#1,441 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
3.1k
Forks
651
Avg merge
15h 2m
Merged PRs (30d)
36

Description

Affected versions: confirmed against the current default branch as of 2026-09-16

Summary

POST /sql-driver/{conversationId}/execute accepts a raw SQL statement and executes it against a configured data source, gated only by [Authorize] (any logged-in user, no role check). It performs no check that the caller owns conversationId, and the underlying ConversationService.SetConversationId call has no ownership check either, silently creating a new conversation using the caller's own user id if the given id doesn't already exist. Any authenticated user, regardless of role, can therefore execute arbitrary SQL against any configured data source by supplying a conversation id of their own choosing.

Details

[Authorize]
[ApiController]
public class SqlDriverController : ControllerBase
{
    [HttpPost]
    [Route("/sql-driver/{conversationId}/execute")]
    public async Task<IActionResult> ExecuteSqlQuery([FromRoute] string conversationId, [FromBody] SqlQueryRequest sqlQueryRequest)
    {
        ...
        var conv = _services.GetRequiredService<IConversationService>();
        await conv.SetConversationId(conversationId, [...]);
        ...
        var result = await fn.InvokeFunction("execute_sql", msg);

ConversationService.SetConversationId:

public async Task SetConversationId(string conversationId, List<MessageState> states, bool isReadOnly = false)
{
    _conversationId = conversationId;
    await _state.Load(_conversationId, isReadOnly);
    states.ForEach(x => _state.SetState(...));
}

No ownership check exists here. By contrast, ConversationController.GetConversation (which only reads a conversation's own dialog, a far less sensitive operation) already enforces the caller is an admin or the conversation's owner:

var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
var filter = new ConversationFilter
{
    Id = conversationId,
    UserId = !isAdmin ? user?.Id : null,
    ...
};

SqlDriverController has no equivalent check anywhere. The identical gap exists on POST /sql-driver/{conversationId}/result.

POC

(available upon request)

Impact

Any authenticated user of a BotSharp instance, regardless of intended role, can execute arbitrary SQL statements against any data source configured in SqlDriverSetting.Connections, by calling this endpoint with a conversation id of their own choosing (no leaked or real conversation id is required). In a deployment using this plugin's intended "chat with your data" feature with real database connections, this grants every logged-in chat user unrestricted database access, bypassing whatever scoping the AI agent's own system prompt was meant to enforce.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the SqlDriverController ExecuteSqlQuery entry point and trace ConversationService.SetConversationId, then compare its authorization context with ConversationController.GetConversation. Review both the /execute and /result paths; done means authenticated users cannot bypass the intended role or conversation-ownership boundary when invoking SQL.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, sql
Domain
authorization, backend-api-design, databases, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.