Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/test-suites/query-security-deployment-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ that possess those capabilities.**
|---|---|---|---|
| PostgreSQL 16 | Dedicated NOSUPERUSER/NOCREATEDB/NOCREATEROLE/NOBYPASSRLS identity; grant CONNECT, USAGE and SELECT only to intended schemas/tables; no membership granting write or privileged routine EXECUTE; restrict search_path/schema CREATE | Same-connection native READ ONLY transaction and pinned pg_catalog resolution; 11-case mandatory Testcontainers Host Query corpus exercises sequence/UDF writes, whitelist escapes, shadowed operators, injection and legitimate CTE controls | Privileged SECURITY DEFINER/dblink/external-side-effect routines that can bypass connection-local read-only |
| MySQL 8.4 InnoDB | Dedicated SELECT-only principal on intended objects, without INSERT/UPDATE/DELETE, CREATE TEMPORARY TABLES, EXECUTE/CREATE ROUTINE, FILE or elevated global roles; protect schemas from attacker-controlled routines | Next-transaction READ ONLY fence; 3-case mandatory live MySQL native/UDF corpus, including real TEMPORARY-function negative control and normal builtins | Native or stored routines performing separate-connection/external I/O; unqualified routines without physical identity proof |
| SQL Server 2022 | Dedicated restricted SQL Login with explicit object SELECT, no DML/DDL, EXECUTE AS/IMPERSONATE, TRUSTWORTHY/UNSAFE assembly privilege, elevated cross-database ownership chains or untrusted CLR EXECUTE | Same-connection effective principal/permission attestation for no-whitelist Query, plus 2-case mandatory native/UDF least-privilege corpus | SQL CLR/external code or privileged cross-database modules outside the attested principal scope |
| SQL Server 2022 | Dedicated restricted SQL Login with explicit object SELECT, no DML/DDL, EXECUTE AS/IMPERSONATE, TRUSTWORTHY/UNSAFE assembly privilege, elevated cross-database ownership chains or untrusted CLR EXECUTE | Same-connection effective principal/permission attestation **and typed policy rejection of opaque qualified/quoted native functions** for no-whitelist Query; 2-case mandatory native/UDF least-privilege corpus | SQL CLR/external code or privileged cross-database modules outside the attested principal scope |
| Oracle 26ai | Dedicated CREATE SESSION plus intended object SELECT grants only; no DML/DDL/ANY privileges or EXECUTE on definer-rights/autonomous/external PL/SQL packages; enforce PDB-scoped roles | Native provider READ ONLY transaction and 4-case mandatory real UDF, builtin and autonomous-transaction negative/positive corpus | Privileged autonomous transactions and external packages not restricted at the DB role boundary |
| Firebird 5 | Dedicated DB user/role with SELECT only on intended relations, no DML/DDL privileges and no EXECUTE on untrusted stored procedures/PSQL/external UDRs | Native READ ONLY transaction and mandatory Firebird real native/UDF whitelist negative control | EXTERNAL/privileged procedures or autonomous effects not sandboxed by the DB role |
| SQLite | Read-only filesystem permissions and a read-only connection for Query-only targets wherever possible; embedding hosts **must not register writable callbacks** or callbacks opening separate privileged connections | File-backed 11-case mandatory Host Query corpus for hidden tables, overridden builtins, same-connection and **separate-connection callback writes**, whitelist and legal CTEs; query_only fence, built-in collision check, and same-connection EXPLAIN attestation of invoked callback opcodes | Untrusted virtual tables, host code outside invoked SQL function opcodes, writable same-process handles or external I/O outside the inspected Query |
Expand Down Expand Up @@ -49,6 +49,7 @@ interpret this guard as authorization for arbitrary external callbacks.

- A Query-only MCP tool grant is **not** a DB privilege grant. A null/empty table allowset means *no table whitelist*, not an empty readable set. To protect confidential rows, configure explicit target DB privileges **and** the intended MCP table allowset. Do not issue production Query keys bound to sysadmin/root/DBA identities.
- Avoid privileged functions whose behavior the compiler cannot prove. Deny their EXECUTE at the database role level and disable/limit extensions, SQL CLR, native UDRs and callbacks as appropriate; fail closed instead of claiming arbitrary code purity.
- SQL Server Query with an absent **or empty** table allowset now rejects opaque qualified/quoted provider-native functions at the typed Host policy stage, before any physical connection. It still requires the independently attested least-privileged SQL Server login for permitted built-ins and table SELECTs. An explicit nonempty allowset retains its existing native-function restriction. Standalone SqlCore parsing/analysis remains compatible. The guard is a bounded defense against ambiguous UDF identity, **not** proof that unqualified functions or arbitrary external modules are pure.
- Use per-provider real positive SELECT plus explicit negative forbidden SELECT/native routine controls before allowing new production credentials. Native functions with external effects are unsupported unless the deployment explicitly constrains them using DB role/host sandbox controls outside the SQL compiler.
- The 32 physical Query cases across the six engines (PostgreSQL 11, SQLite 11, MySQL 3, SQL Server 2, Oracle 4, Firebird 1) are **bounded security evidence**, not a proof of arbitrary third-party routine purity. Each CI owner must prove nonempty, exact pass counts and zero skips.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,15 @@ private static SqlExecutionPlanPolicy CreateQueryPolicy(
securityPolicy.QueryMaxRows,
effectiveTables is null
&& provider is (SqlAgentToolType.MySQL
or SqlAgentToolType.MsSqlServer
or SqlAgentToolType.Oracle or SqlAgentToolType.Firebird)
// All three use a native read-only Query transaction, but
// cannot prove purity of an arbitrary schema-qualified native
// routine: MySQL TEMPORARY writes and Oracle/Firebird
// autonomous or external side effects escape that guarantee.
// Host-only typed policy; standalone SqlCore grammar unchanged.
// Read-only transaction fences (MySQL/Oracle/Firebird) cannot
// prove separate-connection or external routine purity.
// SQL Server's live effective-principal attestation is also
// not a semantic proof of EXECUTE AS/CLR/cross-database UDFs,
// nor protection against a permission race. Reject opaque
// native function identities before opening any connection.
// This is Host-only policy: standalone SqlCore grammar remains.
? SqlQueryNativeFunctionPolicy.RejectOpaqueQualifiedOrQuoted
: SqlQueryNativeFunctionPolicy.PreserveCompilerGrammar);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,26 @@ CREATE FUNCTION dbo.leak_secret() RETURNS NVARCHAR(64)
var sql = "SELECT dbo.leak_secret() AS leaked FROM dbo.users";
Assert.Equal(Secret, (await ScalarAsync(setup, sql, ct))?.ToString());
var before = await PhysicalCountAsync(setup, "dbo.users", "dbo.secrets", ct);

// The same opaque function must be denied at the compiler policy
// boundary even with no whitelist; the physical principal guard
// must not be the only reason this privileged login is rejected.
var provider = new MsSqlServerProvider();
var noWhitelist = Assert.Throws<UnauthorizedAccessException>(() =>
new TypedQueryRuntime().Compile(
provider, sql, SqlAgentToolType.MsSqlServer, Policy(),
allowedTables: null));
Assert.Equal("SQL_POLICY_NATIVE_QUERY_FUNCTION_NOT_PROVEN",
Assert.IsType<SqlDiagnostic>(
noWhitelist.Data["HsSqlAgent.SqlCore.Diagnostic"]).Code);
var noWhitelistTool = await Tool(provider, SqlAgentToolType.MsSqlServer,
target, allowedTable: null).ExecuteQuerySql(sql, ct);
Assert.False(noWhitelistTool.Success);
Assert.Empty(noWhitelistTool.Rows);
Assert.DoesNotContain(Secret, noWhitelistTool.ToString());

await AssertMcpDeniesAsync(
new MsSqlServerProvider(), SqlAgentToolType.MsSqlServer,
provider, SqlAgentToolType.MsSqlServer,
target, "dbo.users", sql, "SELECT id FROM dbo.users", ct);
Assert.Equal(before, await PhysicalCountAsync(setup, "dbo.users", "dbo.secrets", ct));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,45 @@ public void ReadOnlyNativeTransactionProvider_NoWhitelistCannotAuthorizeOpaqueFu
}

[Fact]
public void UnrestrictedSqlServerQuery_NativeFunctionNamesRetainExistingCompilerBehavior()
public void SqlServerQueryWithoutWhitelist_RejectsOpaqueNativeFunctionOnBothClrPaths()
{
// A DB permission probe only attests the connected principal at one
// instant. Do not compile a provider-native routine call when Query
// has no table whitelist: privileged EXECUTE AS, CLR and cross-database
// module effects are not established by the SELECT AST.
var provider = new Mock<ISqlProvider>();
provider.SetupGet(x => x.Type).Returns(SqlAgentToolType.MsSqlServer);
const string opaqueSql = "SELECT dbo.leak_secret() AS leaked FROM dbo.users";
var empty = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

var compiled = new TypedQueryRuntime().Compile(
provider.Object, "SELECT dbo.leak_secret() AS leaked FROM dbo.users",
SqlAgentToolType.MsSqlServer, Policy, allowedTables: null);
foreach (IReadOnlySet<string>? allowed in new IReadOnlySet<string>?[] { null, empty })
{
var diagnosticException = Assert.Throws<UnauthorizedAccessException>(() =>
new TypedQueryRuntime().Compile(
provider.Object, opaqueSql, SqlAgentToolType.MsSqlServer,
Policy, allowed));
var diagnostic = Assert.IsType<SqlDiagnostic>(
diagnosticException.Data["HsSqlAgent.SqlCore.Diagnostic"]);
Assert.Equal("SQL_POLICY_NATIVE_QUERY_FUNCTION_NOT_PROVEN", diagnostic.Code);
Assert.Equal(SqlDiagnosticStage.Policy, diagnostic.Stage);

Assert.Contains("leak_secret", compiled.Sql, StringComparison.OrdinalIgnoreCase);
Assert.Throws<UnauthorizedAccessException>(() =>
new TypedQueryRuntime().CompileWithFacts(
provider.Object, opaqueSql, SqlAgentToolType.MsSqlServer,
Policy, allowed, targetProfile: null));

var builtIn = new TypedQueryRuntime().Compile(
provider.Object, "SELECT ABS(id) AS amount FROM dbo.users",
SqlAgentToolType.MsSqlServer, Policy, allowed);
Assert.Contains("ABS(", builtIn.Sql, StringComparison.OrdinalIgnoreCase);
}

// Host-only policy cannot silently narrow the public standalone
// SqlCore grammar used for analysis and non-MCP consumers.
var standalone = SqlCoreFacade.CompileQuery(
opaqueSql, SqlAgentToolType.MsSqlServer, SqlAgentToolType.MsSqlServer,
new SqlPlanValidationContext("standalone-sqlserver-grammar"),
new SqlExecutionPlanPolicy(20));
Assert.Contains("leak_secret", standalone.Sql, StringComparison.OrdinalIgnoreCase);
}
}
Loading