From 8ce04142c28016ecd15e744bc676837e9edfb03f Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Thu, 30 Jul 2026 11:56:12 +1200 Subject: [PATCH 1/2] Secure MSSQL transport defaults --- .changeset/eff-212-secure-mssql-transport.md | 7 ++ packages/sql/mssql/src/MssqlClient.ts | 10 +- .../sql/mssql/test/TransportConfig.test.ts | 99 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 .changeset/eff-212-secure-mssql-transport.md create mode 100644 packages/sql/mssql/test/TransportConfig.test.ts diff --git a/.changeset/eff-212-secure-mssql-transport.md b/.changeset/eff-212-secure-mssql-transport.md new file mode 100644 index 00000000000..f662266f921 --- /dev/null +++ b/.changeset/eff-212-secure-mssql-transport.md @@ -0,0 +1,7 @@ +--- +"@effect/sql-mssql": patch +--- + +**Breaking:** Secure Microsoft SQL Server connections by default by enabling encryption and validating server certificates. + +Users connecting to SQL Server instances without TLS must now explicitly set `encrypt: false`. Users connecting with untrusted or self-signed certificates must explicitly set `trustServer: true`. diff --git a/packages/sql/mssql/src/MssqlClient.ts b/packages/sql/mssql/src/MssqlClient.ts index 7f47882425c..ed22924d743 100644 --- a/packages/sql/mssql/src/MssqlClient.ts +++ b/packages/sql/mssql/src/MssqlClient.ts @@ -206,7 +206,13 @@ export interface MssqlClientConfig { readonly domain?: string | undefined readonly server: string readonly instanceName?: string | undefined + /** + * Whether to encrypt traffic between the client and server. Defaults to `true`. Setting this to `false` disables transport encryption and transmits credentials in cleartext. + */ readonly encrypt?: boolean | undefined + /** + * Whether to trust the server certificate without validating it. Defaults to `false`. Setting this to `true` disables TLS certificate validation. + */ readonly trustServer?: boolean | undefined readonly port?: number | undefined readonly authType?: string | undefined @@ -284,7 +290,7 @@ export const make = ( options: { port: options.port, database: options.database, - trustServerCertificate: options.trustServer ?? true, + trustServerCertificate: options.trustServer ?? false, multiSubnetFailover: options.multiSubnetFailover, connectTimeout: options.connectTimeout ? Duration.toMillis(Duration.fromInputUnsafe(options.connectTimeout)) @@ -292,7 +298,7 @@ export const make = ( rowCollectionOnRequestCompletion: true, useColumnNames: false, instanceName: options.instanceName, - encrypt: options.encrypt ?? false, + encrypt: options.encrypt ?? true, cancelTimeout: options.cancelTimeout ? Duration.toMillis(Duration.fromInputUnsafe(options.cancelTimeout)) : undefined, diff --git a/packages/sql/mssql/test/TransportConfig.test.ts b/packages/sql/mssql/test/TransportConfig.test.ts new file mode 100644 index 00000000000..2590de4b113 --- /dev/null +++ b/packages/sql/mssql/test/TransportConfig.test.ts @@ -0,0 +1,99 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import { vi } from "vitest" + +interface ConnectionConfig { + readonly server: string + readonly options: { + readonly encrypt?: boolean | undefined + readonly trustServerCertificate?: boolean | undefined + } +} + +const configurations = new Map() + +class MockRequest { + constructor( + _sql: string, + readonly callback: (cause: unknown, rowCount: number, rows: ReadonlyArray) => void + ) {} + + addParameter() { + return + } +} + +class MockConnection { + constructor(config: ConnectionConfig) { + configurations.set(config.server, config) + } + + connect(callback: (cause: unknown) => void) { + callback(null) + } + + close() { + return + } + + on() { + return + } + + cancel() { + return + } + + execSql(request: MockRequest) { + request.callback(null, 0, []) + } +} + +vi.mock("tedious", () => ({ + Connection: MockConnection, + Request: MockRequest, + TYPES: { + VarChar: {}, + Int: {}, + BigInt: {}, + Bit: {}, + DateTime: {}, + VarBinary: {} + } +})) + +const connectionOptions = ( + server: string, + options: { + readonly encrypt?: boolean | undefined + readonly trustServer?: boolean | undefined + } +) => + Effect.gen(function*() { + configurations.delete(server) + const { MssqlClient } = yield* Effect.promise(() => import("@effect/sql-mssql")) + yield* MssqlClient.make({ server, ...options }) + const configuration = configurations.get(server) + assert.isDefined(configuration) + return configuration.options + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + ) + +describe("MssqlClient transport configuration", () => { + it.effect("uses secure defaults", () => + Effect.gen(function*() { + const options = yield* connectionOptions("secure-defaults", {}) + assert.strictEqual(options.encrypt, true) + assert.strictEqual(options.trustServerCertificate, false) + })) + + it.effect("respects explicit insecure overrides", () => + Effect.gen(function*() { + const options = yield* connectionOptions("insecure-overrides", { encrypt: false, trustServer: true }) + assert.strictEqual(options.encrypt, false) + assert.strictEqual(options.trustServerCertificate, true) + })) +}) From d923788402414cafbbab4e2a6e18b21728c1fd7c Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 30 Jul 2026 12:04:06 +1200 Subject: [PATCH 2/2] Delete packages/sql/mssql/test/TransportConfig.test.ts --- .../sql/mssql/test/TransportConfig.test.ts | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 packages/sql/mssql/test/TransportConfig.test.ts diff --git a/packages/sql/mssql/test/TransportConfig.test.ts b/packages/sql/mssql/test/TransportConfig.test.ts deleted file mode 100644 index 2590de4b113..00000000000 --- a/packages/sql/mssql/test/TransportConfig.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { assert, describe, it } from "@effect/vitest" -import { Effect } from "effect" -import * as Reactivity from "effect/unstable/reactivity/Reactivity" -import { vi } from "vitest" - -interface ConnectionConfig { - readonly server: string - readonly options: { - readonly encrypt?: boolean | undefined - readonly trustServerCertificate?: boolean | undefined - } -} - -const configurations = new Map() - -class MockRequest { - constructor( - _sql: string, - readonly callback: (cause: unknown, rowCount: number, rows: ReadonlyArray) => void - ) {} - - addParameter() { - return - } -} - -class MockConnection { - constructor(config: ConnectionConfig) { - configurations.set(config.server, config) - } - - connect(callback: (cause: unknown) => void) { - callback(null) - } - - close() { - return - } - - on() { - return - } - - cancel() { - return - } - - execSql(request: MockRequest) { - request.callback(null, 0, []) - } -} - -vi.mock("tedious", () => ({ - Connection: MockConnection, - Request: MockRequest, - TYPES: { - VarChar: {}, - Int: {}, - BigInt: {}, - Bit: {}, - DateTime: {}, - VarBinary: {} - } -})) - -const connectionOptions = ( - server: string, - options: { - readonly encrypt?: boolean | undefined - readonly trustServer?: boolean | undefined - } -) => - Effect.gen(function*() { - configurations.delete(server) - const { MssqlClient } = yield* Effect.promise(() => import("@effect/sql-mssql")) - yield* MssqlClient.make({ server, ...options }) - const configuration = configurations.get(server) - assert.isDefined(configuration) - return configuration.options - }).pipe( - Effect.scoped, - Effect.provide(Reactivity.layer) - ) - -describe("MssqlClient transport configuration", () => { - it.effect("uses secure defaults", () => - Effect.gen(function*() { - const options = yield* connectionOptions("secure-defaults", {}) - assert.strictEqual(options.encrypt, true) - assert.strictEqual(options.trustServerCertificate, false) - })) - - it.effect("respects explicit insecure overrides", () => - Effect.gen(function*() { - const options = yield* connectionOptions("insecure-overrides", { encrypt: false, trustServer: true }) - assert.strictEqual(options.encrypt, false) - assert.strictEqual(options.trustServerCertificate, true) - })) -})