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>
55 lines
3.1 KiB
C#
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));
|
|
}
|