feat(mssql): add Microsoft SQL Server integration - #6739
Conversation
Add a Microsoft SQL Server block backed by six tools (query, execute, insert, update, delete, introspect), mirroring the existing PostgreSQL and MySQL integrations. Connections go through the `mssql` (Tedious) driver: `connectionTimeout` is top-level while `encrypt`, `trustServerCertificate`, and `instanceName` live under `options`, and `port` is omitted when a named instance is used. Values are bound as `@paramN` via `request.input()`; no user value is interpolated into SQL. Identifiers are bracket-quoted after validation and WHERE clauses run through the shared injection guard. Introspection reads INFORMATION_SCHEMA plus the `sys.indexes` catalog views for tables, columns, primary keys, foreign keys, and indexes. The icon is a placeholder database cylinder drawn with `currentColor` until the real brand mark lands. Requires `bun install` for the new `mssql` / `@types/mssql` deps.
…g reads Tedious exposes `options.connector`, a hook that replaces its own resolve-and-connect path, so the connection can be pinned to the address `validateDatabaseHost` already approved instead of re-resolving the hostname. `server` stays the hostname because tedious derives the TLS `servername` from it independently of the connector, so SNI and certificate validation survive the pin. This brings MSSQL in line with the PostgreSQL and MySQL tools. Named instances are dropped: tedious resolves them with a UDP SQL Server Browser lookup issued outside the connector, and node-mssql deletes `port` whenever `instanceName` is set, so no configuration leaves a named instance pinned. A named instance is reachable through its static TCP port. Introspection fixes: - index key columns now filter on `key_ordinal > 0`; INCLUDEd columns and partitioning columns both report `0` and were being returned as key columns, ordered ahead of the real ones - foreign keys resolve through `sys.foreign_keys` / `sys.foreign_key_columns` rather than `INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS`, whose join to `TABLE_CONSTRAINTS` has no row when a key references a unique index and so dropped the key entirely - `is_unique` is a `bit`, which tedious maps to a boolean, so it is coerced rather than compared - schemas come from `sys.schemas`, which needs only `public` and carries no metadata-visibility caveat The WHERE-clause guard also covers `WAITFOR TIME`, `OPENQUERY`, `OPENXML`, the legacy `master..sys*` compatibility views, and extended and OLE-automation procedures beyond `xp_cmdshell`. Regenerates the docs and catalog artifacts the icon change left stale.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Query vs execute: The Query operation is restricted to read-only Connections: Uses Introspection: Schema metadata uses Also adds Reviewed by Cursor Bugbot for commit 340ca6b. Configure here. |
|
@cursor review |
Greptile SummaryThe PR adds a complete Microsoft SQL Server integration spanning six tools, authenticated API routes, SSRF-pinned connections, schema introspection, block and registry metadata, documentation, and dependencies. The latest fixes strengthen the SELECT-only and WHERE screens, but lexical gaps still permit hidden statements.
Confidence Score: 3/5The PR is not yet safe to merge because the Query validator can still hide mutations and the Update/Delete WHERE validator can execute an appended SELECT. The read-only validator misinterprets quotes inside SQL Server bracketed identifiers and then executes the original batch, while the WHERE screen omits SELECT despite directly interpolating accepted conditions into T-SQL batches. Files Needing Attention: apps/sim/app/api/tools/mssql/utils.ts, apps/sim/app/api/tools/mssql/query/route.ts, apps/sim/app/api/tools/mssql/update/route.ts, apps/sim/app/api/tools/mssql/delete/route.ts
|
| Filename | Overview |
|---|---|
| apps/sim/app/api/tools/mssql/utils.ts | Implements connection pinning, query construction, validation, execution, and introspection, but its lexical masking and statement list leave two reachable statement-screening gaps. |
| apps/sim/app/api/tools/mssql/query/route.ts | Correctly invokes read-only validation before connecting, although an accepted masking bypass still reaches execution. |
| apps/sim/app/api/tools/mssql/update/route.ts | Authenticates and closes connections correctly, but returns recordsets produced by a trailing SELECT accepted through the WHERE screen. |
| apps/sim/app/api/tools/mssql/delete/route.ts | Shares the update route's validated-WHERE execution path and trailing-recordset behavior. |
| apps/sim/blocks/blocks/mssql.ts | Defines the MSSQL block operations and maps their parameters to the newly registered tools. |
| apps/sim/package.json | Adds exact MSSQL runtime and type dependencies with corresponding lockfile resolutions. |
| bun.lock | Contains the requested mssql, type package, tedious, and transitive dependency resolutions. |
Sequence Diagram
sequenceDiagram
participant Workflow
participant Tool as MSSQL Tool
participant Route as Authenticated API Route
participant Guard as Input Validation
participant SSRF as Host Validation
participant SQL as SQL Server
Workflow->>Tool: Resolved block parameters
Tool->>Route: Internal authenticated request
Route->>Guard: Validate query or WHERE clause
Route->>SSRF: Resolve and approve database host
SSRF-->>Route: Pinned IP address
Route->>SQL: Connect through pinned socket
Route->>SQL: Execute validated T-SQL
SQL-->>Route: Rows and affected counts
Route-->>Workflow: Normalized tool response
Reviews (5): Last reviewed commit: "fix(mssql): reject SQL comments in the r..." | Re-trigger Greptile
The block label, tool description, and docs all present Query as SELECT-only while the route ran whatever T-SQL it was given, so an agent picking mssql_query because "it is only a SELECT" could delete rows. Screen the statement for mutating keywords with string literals stripped, which also catches the WITH ... DELETE form that a leading-token check would miss. Also switch the tool barrel to absolute imports per the repo convention.
…ss batch gap The local validateWhereClause re-derived an older copy of the shared patterns and scanned raw text, so it missed a bare 1=1 and false-positived on prose in a quoted value. Delegate to validateSqlWhereClause, which masks string literals first, and keep only the SQL Server surfaces it has no reason to know about. T-SQL needs no statement terminator, so every semicolon-anchored stacked-query check reads straight past `id = 1 DROP TABLE dbo.users`. Screen for a bare statement-introducing keyword to close that; word boundaries leave ordinary column names like updated_at and deleted_at untouched. Export maskSqlStringLiterals so the dialect layer masks the same way the shared guard does rather than carrying a weaker single-quote-only copy.
|
@cursor review |
…d-only path The previous round left two keyword lists maintained separately, and both were short: DBCC, KILL, CHECKPOINT, USE, and DENY were in neither, so `SELECT 1; DBCC SHRINKDATABASE(...)` and `id = 1 DBCC SHRINKDATABASE(...)` both got through. Collapse them into one MSSQL_STATEMENT_KEYWORDS shared by the query and WHERE screens so a keyword cannot be covered in one place and missed in the other, and add the administrative commands. Also reject any second statement after a semicolon in the Query path outright. That closes SELECT 1; <anything> structurally instead of by naming the anything, so the blacklist no longer has to be exhaustive to hold.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ecb1dac. Configure here.
A block comment placed inside a keyword splits it as far as a lexical scan is concerned, so keyword coverage cannot settle whether the server rejoins the halves. Refuse comments in the Query path instead of modelling the tokenizer. A SELECT sent through this operation has no need for one, and Execute Raw SQL still accepts them. Masking leaves comment markers intact, so a literal containing -- still passes.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 340ca6b. Configure here.
| error: | ||
| 'The Query operation does not accept SQL comments, because a comment can split a keyword. Use the Execute Raw SQL operation if the statement needs one.', | ||
| } | ||
| } |
There was a problem hiding this comment.
Bracketed identifier hides mutations
When a SELECT contains a single quote inside a bracketed identifier followed by a semicolonless mutation, such as SELECT [a'] DELETE FROM dbo.items, maskSqlStringLiterals treats that quote as the start of a string and hides the remaining batch from these checks. The route then executes the original unmasked query, allowing a mutation through the SELECT-only operation.
| MSSQL_STATEMENT_KEYWORDS.test(masked) || | ||
| MSSQL_PROCEDURE_PATTERN.test(masked) || | ||
| MSSQL_CATALOG_PATTERNS.some((pattern) => pattern.test(masked)) |
There was a problem hiding this comment.
Trailing SELECT bypasses WHERE screening
When an update or delete condition contains a semicolonless trailing SELECT, such as id = 1 SELECT secret FROM dbo.credentials, the shared guard accepts it because it is neither semicolon-prefixed nor a UNION, and this MSSQL keyword screen omits SELECT. The builder interpolates the condition into the batch and the route returns the trailing statement's recordset, exposing rows from an unrelated readable table.
Summary
options.connector, matching how the PostgreSQL and MySQL tools close the DNS-rebinding window.serverstays the original hostname so SNI and certificate validation are unaffectedsys.*catalog views rather thanINFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS, which silently drops any FK that references a unique index instead of a constraint@param1,@param2, …); identifiers are bracket-quoted after a strict character check; WHERE clauses are screened for stacked queries, UNION injection, comment truncation, tautologies,WAITFOR, catalog probes, andxp_/sp_proceduresNamed instances are not supported — please read
This branch deliberately does not support
instanceName, and that is a security decision rather than a gap.Resolving a named instance is a SQL Server Browser UDP/1434 request issued against the hostname, outside the connector. It therefore never goes through the pinned address, and node-mssql additionally deletes
portwheneverinstanceNameis set — so there is no configuration in which a named instance stays pinned to the IP the SSRF guard approved. Supporting it would reintroduce exactly the rebinding window the connector exists to close.Named instances now require a static TCP port. Configure the instance with a fixed port and connect to
host+port.If the team would rather take the exposure than the ergonomic cost, this is a one-commit revert — the restriction is confined to
createMSSQLConnectionand the block's connection subBlocks.Dependencies
mssql(12.7.0) and@types/mssql(12.3.0) were added toapps/sim/package.json, andbun.lockcarries them plus thetedious→@azure/identitytree they pull in (the lockfile diff is entirely that subtree — no unrelated resolutions moved).Anyone checking this branch out needs to run
bun installbefore it type-checks or runs; without itapp/api/tools/mssql/utils.tsreportsCannot find module 'mssql'.Type of Change
Testing
Nothing was executed against a live SQL Server. All verification is doc-derived: every endpoint, catalog view, response field, and driver behavior above is cited against the Microsoft Learn system-view docs and the tedious/node-mssql API docs in TSDoc at the call site. Repo gates pass:
lint,check:api-validation,tool-metadata:check,integration-catalog:check, andgenerate-docsreports no drift.Before merge this wants one pass against a real instance — in particular the introspection result shape on a low-privilege login, where the metadata-visibility filter returns a silently partial result rather than an error.
Checklist