Files
mcp-microsoft/tools/Azure.Mcp.Tools.Postgres/tests/Azure.Mcp.Tools.Postgres.Tests/Validation/SqlQueryValidatorTests.cs
T
1fa503200a Mark MySQL/PostgreSQL database query as destructive and drop SQL verb blocklists (#3202)
* Fixed input validation issue in MySql and Postgres

* Added MySql validation test cases

* Fix Postgres SQL validator: tokenize string-stripped content and add regex timeout

- Tokenize withoutStrings instead of decodedForValidation to avoid false
  positives when dangerous keywords appear inside string literals
  (e.g., SELECT 'drop table' AS msg)
- Only strip U&'...' (string literals) not U&"..." (identifiers) per
  PostgreSQL spec, so obfuscated function names in identifiers are still
  caught by tokenization
- Add RegexHelper.DefaultRegexTimeout to DecodeUnicodeEscapes for
  consistency with other regex usage in the validator
- Fix whitespace formatting in SqlQueryValidatorTests.cs closing brace
- Move U&'...' test case to safe queries (string literals cannot invoke
  functions per PostgreSQL spec)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Marked MySql and Postgres query commands as destructive, not idempotent, and not read-only

* Block the uescape command in validator

* Remove SQL verb/keyword blocklists from MySQL and Postgres query validation

The MySQL and Postgres database query tools are now marked destructive, so
rejecting statements by SQL verb or dangerous-identifier blocklist is no longer
meaningful - the signed-in user's database permissions are the authority.

Validation is reduced to structural checks: empty input, max length, SQL
comments, and multiple/stacked statements. The Unicode/National-string
de-obfuscation logic (which only existed to feed the blocklists) is removed,
along with the unfinished UESCAPE check that did not compile.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Reword changelog entries to fix cspell failure

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Move MySQL/Postgres query tools to a destructive consolidated tool

The consolidated tool discovery strategy requires every mapped command's
ToolMetadata to match its consolidated tool exactly. Marking the query
commands destructive left them mismatched against the read-only
get_azure_databases_details tool, which made the server fail to start in
consolidated mode.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Test consolidated tool metadata mappings

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Vinay Gera <vigera@microsoft.com>
2026-09-11 15:27:05 -07:00

79 lines
3.3 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Azure.Mcp.Tools.Postgres.Validation;
using Microsoft.Mcp.Core.Commands;
using Xunit;
namespace Azure.Mcp.Tools.Postgres.Tests.Validation;
public class SqlQueryValidatorTests
{
[Theory]
[InlineData("SELECT * FROM users LIMIT 100")]
[InlineData("SELECT COUNT(*) FROM products LIMIT 1")]
[InlineData("SELECT COUNT(*) FROM products;")]
[InlineData("SELECT * FROM users WHERE name = 'foo--bar'")]
[InlineData("SELECT * FROM users WHERE name = 'back\\\\slash'")]
[InlineData("SELECT * FROM users WHERE name = E'it\\'s a test'")]
[InlineData("SELECT * FROM users WHERE name = E'back\\\\slash'")]
[InlineData("SELECT * FROM users WHERE data = '/* not a comment */'")]
[InlineData("SELECT * FROM user_deletions")]
[InlineData("SELECT * FROM datasets")]
[InlineData("SELECT * FROM reunion_events")]
[InlineData("SELECT preset FROM config")]
[InlineData("SELECT * FROM intersections")]
[InlineData("SELECT * FROM exceptions")]
[InlineData("SELECT 'drop table' AS msg FROM users")]
[InlineData("SELECT * FROM users WHERE note = 'pg_sleep is bad'")]
[InlineData("SELECT 1 UNION SELECT 2")]
[InlineData("SELECT * FROM users WHERE id = 1 or 1=1")]
[InlineData("INSERT INTO users VALUES (1)")]
[InlineData("UPDATE users SET name = 'x'")]
[InlineData("DELETE FROM users WHERE id = 1")]
[InlineData("DROP TABLE users")]
public void ValidateQuery_WithAcceptedQueries_ShouldNotThrow(string query)
{
SqlQueryValidator.ValidateQuery(query);
}
[Theory]
[InlineData("SELECT 1 -- line comment")]
[InlineData("SELECT 1 /* block comment */")]
[InlineData("SELECT 'foo\\' /* ' FROM pg_shadow --'")] // backslash does not escape quotes in standard strings
[InlineData("SELECT * FROM users WHERE name = 'it\\'s a test -- ok'")] // \' is not an escape in standard SQL
public void ValidateQuery_WithComments_ShouldThrow(string query)
{
var exception = Assert.Throws<CommandValidationException>(() => SqlQueryValidator.ValidateQuery(query));
Assert.Contains("Comments are not allowed", exception.Message);
}
[Theory]
[InlineData("SELECT * FROM users; DROP TABLE users")]
[InlineData("SELECT * FROM users; SELECT * FROM products")]
public void ValidateQuery_WithMultipleStatements_ShouldThrow(string query)
{
var exception = Assert.Throws<CommandValidationException>(() => SqlQueryValidator.ValidateQuery(query));
Assert.Contains("Multiple or stacked SQL statements are not allowed", exception.Message);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void ValidateQuery_WithEmptyOrNullQuery_ShouldThrow(string? query)
{
var exception = Assert.Throws<CommandValidationException>(() => SqlQueryValidator.ValidateQuery(query));
Assert.Contains("Query cannot be empty", exception.Message);
}
[Fact]
public void ValidateQuery_WithLongQuery_ShouldThrow()
{
var longQuery = "SELECT * FROM users WHERE " + new string('X', 5001);
var exception = Assert.Throws<CommandValidationException>(() => SqlQueryValidator.ValidateQuery(longQuery));
Assert.Contains("Query length exceeds limit", exception.Message);
}
}