sqlinsight-pro/tests/SQLInsight.Core.Tests/ReadOnlyQueryTests.cs
Søren Eggert Lundsteen Olsen 8d092b6a35 Review 1.5: the read-only check of typed queries can no longer be bypassed
The synthetic SQL check (and custom rules) used a regex that ended an [identifier]
at the first ] and a block comment at the first */, so "[x]]'] DELETE ..." hid a
DELETE inside what it took for a string. T-SQL also needs no semicolon between
statements, so "SELECT 1 COMMIT" or "SELECT 1 WRITETEXT ..." passed.

- ReadOnlyQuery lexes T-SQL as SQL Server does (nested comments, '' ]] ""),
  refuses unterminated comments/strings/identifiers, every statement keyword
  (COMMIT, ROLLBACK, SET, DECLARE, WRITETEXT, UPDATETEXT, RAISERROR, CHECKPOINT,
  ENABLE/DISABLE, RECEIVE/SEND ...), NEXT VALUE FOR (sequences are not rolled
  back) and update/exclusive lock hints, and a second SELECT unless joined by
  UNION/EXCEPT/INTERSECT.
- CustomRuleDefinition.QueryProblems uses it, so the synthetic check validates the
  same way when saved and before it runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:08:34 +02:00

55 lines
3.1 KiB
C#

using SQLInsight.Core.CustomRules;
namespace SQLInsight.Core.Tests;
/// <summary>A typed query (custom rule, synthetic SQL check) is one read-only SELECT, also when someone tries to hide more.</summary>
public class ReadOnlyQueryTests
{
[Theory]
// A ]] in an [identifier] is not its end: the ' after it is no string, and the DELETE is code.
[InlineData("SELECT 1 AS [x]]'] ; DELETE FROM dbo.t; SELECT 'a'")]
[InlineData("SELECT 1 AS [x]]'] DELETE FROM dbo.t SELECT 'a'")]
// Block comments nest in T-SQL; an unterminated one is refused.
[InlineData("SELECT 1 /* /* */ DELETE FROM dbo.t")]
[InlineData("SELECT 1 AS \"a\"\"'\" DELETE FROM dbo.t --'")]
// No semicolon is needed between statements.
[InlineData("SELECT 1 COMMIT")]
[InlineData("SELECT 1 SELECT 2")]
[InlineData("SELECT 1 WRITETEXT dbo.t.c @p 'x'")]
[InlineData("SELECT 1 UPDATETEXT dbo.t.c @p 0 NULL 'x'")]
[InlineData("SELECT 1 DECLARE @x int")]
[InlineData("SELECT 1 SET CONTEXT_INFO 0x01")]
[InlineData("SELECT 1 RAISERROR('x', 16, 1) WITH LOG")]
[InlineData("SELECT 1 CHECKPOINT")]
[InlineData("SELECT 1 ROLLBACK")]
[InlineData("SELECT 1 BEGIN TRAN")]
[InlineData("SELECT 1 ENABLE TRIGGER trg ON dbo.t")]
[InlineData("SELECT 1 WHILE 1 = 1 SELECT 2")]
[InlineData("WITH q AS (SELECT 1 AS a) SELECT a FROM q SELECT 2")]
[InlineData("SELECT 1 x RECEIVE TOP (1) * FROM dbo.q")]
[InlineData("SELECT 1 ADD SIGNATURE TO dbo.p BY CERTIFICATE c")]
// Side effects inside one SELECT: sequences are not rolled back; update and exclusive locks block production.
[InlineData("SELECT NEXT VALUE FOR dbo.InvoiceNumbers")]
[InlineData("SELECT * FROM dbo.CustTable WITH (UPDLOCK, HOLDLOCK)")]
[InlineData("SELECT * FROM dbo.CustTable WITH (TABLOCKX)")]
[InlineData("SELECT * FROM dbo.CustTable WITH (XLOCK)")]
[InlineData("SELECT * INTO dbo.Copy FROM dbo.CustTable")]
[InlineData("SELECT 1 EXEC sp_who")]
[InlineData("SELECT * FROM OPENQUERY(LINKED, 'EXEC x')")]
[InlineData("select 1 exec('delete dbo.t')")]
[InlineData("SELECT 'unterminated")]
[InlineData("SELECT [unterminated")]
public void Hidden_or_extra_statements_are_refused(string query) => Assert.NotEmpty(ReadOnlyQuery.Problems(query));
[Theory]
[InlineData("SELECT 1")]
[InlineData("SELECT COUNT(*) FROM dbo.DATAAREA")]
[InlineData("SELECT [update], [into], [x]]y] FROM dbo.audit")]
[InlineData("SELECT 'DELETE; EXEC ''x''' AS text /* INSERT /* nested */ COMMIT */ -- DROP")]
[InlineData("WITH d AS (SELECT name FROM sys.databases), e (n) AS (SELECT 1) SELECT * FROM d;")]
[InlineData("SELECT name FROM sys.databases UNION ALL SELECT name FROM sys.objects EXCEPT SELECT N'x' INTERSECT SELECT N'x'")]
[InlineData("SELECT TOP (10) a FROM (SELECT 1 AS a) x WHERE a IN (SELECT 1) ORDER BY a OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY")]
[InlineData("SELECT * FROM dbo.CustTable WITH (NOLOCK) OPTION (MAXDOP 1)")]
[InlineData("SELECT IIF(1 = 1, GETDATE(), NULL), \"quoted\"\"name\" FROM sys.objects")]
public void Read_only_queries_are_accepted(string query) => Assert.Empty(ReadOnlyQuery.Problems(query));
}