From 581797722b0c1ae9d37632d369dfb733956c9b6e Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Fri, 8 May 2026 09:49:01 -0500 Subject: [PATCH 1/2] security: address GitHub code scanning and secret scanning alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit `permissions: contents: read` to test.yml and publish.yml workflows. Grant `contents: write` on the publish job for releases. - Redact password / SQL payloads in protocol-handler debug logs so the password message and credential-bearing SQL never hit clear-text output. Same redaction applied to lib/parser.ts logging. - Replace Math.random() with crypto.randomInt for the backend secret key in BackendKeyData — that key authenticates CancelRequest, so it needs to be unguessable. - Stop logging full SQL strings in postgres-server integration test mocks; log query length only. - Replace `mongodb+srv://username:password@cluster.mongodb.net` style placeholders in three blog posts so they no longer trip the Atlas URI secret scanner. Switched code samples to env vars and the SQL sample to `:` placeholders. --- .github/workflows/publish.yml | 5 +++ .github/workflows/test.yml | 3 ++ ...ngodb-atlas-cloud-deployment-management.md | 2 +- .../mongodb-gridfs-file-management-sql.md | 2 +- ...rch-ai-applications-semantic-similarity.md | 2 +- packages/lib/src/parser.ts | 16 ++++++- .../postgres-server/src/protocol-handler.ts | 45 ++++++++++++++----- .../integration/minimal-integration.test.ts | 2 +- .../integration/minimal.integration.test.ts | 2 +- 9 files changed, 63 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c8941ad..4d20da3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,9 +5,14 @@ on: tags: - 'v*' # Run on any tag that starts with v (e.g., v1.0.0) +permissions: + contents: read + jobs: publish: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 816c480..d7d7a64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ main ] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md b/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md index 9d5eb0e..b0ad86b 100644 --- a/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md +++ b/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md @@ -1339,7 +1339,7 @@ QueryLeaf provides seamless integration with MongoDB Atlas through familiar SQL -- QueryLeaf Atlas connection and management -- Connect to Atlas cluster with connection string CONNECT TO atlas_cluster WITH ( - connection_string = 'mongodb+srv://username:password@cluster.mongodb.net/database', + connection_string = 'mongodb+srv://:@cluster.mongodb.net/database', read_preference = 'secondaryPreferred', write_concern = 'majority', max_pool_size = 50, diff --git a/docs/blog/posts/mongodb-gridfs-file-management-sql.md b/docs/blog/posts/mongodb-gridfs-file-management-sql.md index da0e8c4..2f4c712 100644 --- a/docs/blog/posts/mongodb-gridfs-file-management-sql.md +++ b/docs/blog/posts/mongodb-gridfs-file-management-sql.md @@ -262,7 +262,7 @@ const fs = require('fs'); const crypto = require('crypto'); const path = require('path'); -const client = new MongoClient('mongodb+srv://username:password@cluster.mongodb.net'); +const client = new MongoClient(process.env.MONGODB_URI); const db = client.db('file_storage_platform'); // Advanced GridFS file management system diff --git a/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md b/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md index 436bdf8..f6439fd 100644 --- a/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md +++ b/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md @@ -284,7 +284,7 @@ const { MongoClient } = require('mongodb'); const { OpenAI } = require('openai'); const tf = require('@tensorflow/tfjs-node'); -const client = new MongoClient('mongodb+srv://username:password@cluster.mongodb.net'); +const client = new MongoClient(process.env.MONGODB_URI); const db = client.db('advanced_ai_search_platform'); // Advanced AI-powered search and recommendation engine diff --git a/packages/lib/src/parser.ts b/packages/lib/src/parser.ts index 37a560d..66b9a2f 100644 --- a/packages/lib/src/parser.ts +++ b/packages/lib/src/parser.ts @@ -2,7 +2,21 @@ import { From, Parser as NodeSqlParser } from 'node-sql-parser'; import { SqlParser, SqlStatement } from './interfaces'; import debug from 'debug'; -const log = debug('queryleaf:parser'); +const rawLog = debug('queryleaf:parser'); + +// SQL fed to the parser may contain literal credentials (CREATE USER ... PASSWORD '...'). +// Strip them before they hit debug output. +function redactSql(sql: string): string { + return sql.replace( + /\b(PASSWORD|IDENTIFIED\s+BY|IDENTIFIED\s+WITH\s+\S+\s+AS)\s+('([^']|'')*'|"([^"]|"")*"|\S+)/gi, + '$1 ***' + ); +} + +function log(message: string, ...args: unknown[]): void { + const safeArgs = args.map((arg) => (typeof arg === 'string' ? redactSql(arg) : arg)); + rawLog(message, ...safeArgs); +} // Custom PostgreSQL mode with extensions to support our syntax needs const CUSTOM_DIALECT = { diff --git a/packages/postgres-server/src/protocol-handler.ts b/packages/postgres-server/src/protocol-handler.ts index db75d03..9f2bb37 100644 --- a/packages/postgres-server/src/protocol-handler.ts +++ b/packages/postgres-server/src/protocol-handler.ts @@ -1,11 +1,36 @@ import { Socket } from 'net'; import { QueryLeaf } from '@queryleaf/lib'; import { Transform } from 'stream'; +import { randomInt } from 'crypto'; import debugLib from 'debug'; import { MongoClient, Document } from 'mongodb'; const debug = debugLib('queryleaf:pg-server:protocol'); +// SQL statements may carry literal credentials (e.g., CREATE USER ... PASSWORD '...'). +// Mask them before they reach debug logs. +function redactSql(sql: string | undefined): string { + if (!sql) return ''; + return sql.replace( + /\b(PASSWORD|IDENTIFIED\s+BY|IDENTIFIED\s+WITH\s+\S+\s+AS)\s+('([^']|'')*'|"([^"]|"")*"|\S+)/gi, + '$1 ***' + ); +} + +// Strip password / query payloads from a parsed client message before logging. +function redactMessage(message: { type?: string; string?: string; query?: string }): object { + const { string: _str, query, ...rest } = message; + const safe: Record = { ...rest }; + if (message.type === 'password') { + safe.string = '***'; + } else if (message.type === 'query' || message.type === 'parse') { + safe.query = redactSql(query ?? message.string); + } else if (message.string !== undefined) { + safe.string = message.string; + } + return safe; +} + // Simplified protocol implementation for demo purposes interface BackendMessage { // Buffer containing the formatted message ready to send @@ -564,7 +589,7 @@ export class ProtocolHandler { this.buffer = this.buffer.subarray(message.length); debug(`Message processed, remaining buffer length: ${this.buffer.length}`); - debug('Received message type:', messageType, message); + debug('Received message type:', messageType, redactMessage(message)); // Handle the message this.handleMessage(message.type as MessageName, message); @@ -634,7 +659,7 @@ export class ProtocolHandler { * Handle a startup message */ private handleStartup(message: ClientMessage): void { - debug('Startup message:', message); + debug('Startup message received'); // Extract user and database from parameters if (message.parameters) { @@ -795,7 +820,7 @@ export class ProtocolHandler { * Handle a query message */ private async handleQuery(queryString: string): Promise { - debug('Query message:', queryString); + debug('Query message:', redactSql(queryString)); if (!this.authenticated) { debug('Not authenticated, rejecting query'); @@ -912,7 +937,7 @@ export class ProtocolHandler { private handleParse(message: ClientMessage): void { const { name, query } = message; - debug('Parse message:', name, query); + debug('Parse message:', name, redactSql(query)); try { // Store the prepared statement for later @@ -931,7 +956,7 @@ export class ProtocolHandler { * Handle a bind message */ private handleBind(message: ClientMessage): void { - debug('Bind message:', message); + debug('Bind message:', redactMessage(message)); // In a real implementation, you would bind parameters to a prepared statement // For now, just acknowledge the bind @@ -942,7 +967,7 @@ export class ProtocolHandler { * Handle a describe message */ private handleDescribe(message: ClientMessage): void { - debug('Describe message:', message); + debug('Describe message:', redactMessage(message)); const type = message.string; const name = message.name; @@ -969,7 +994,7 @@ export class ProtocolHandler { * Handle an execute message */ private async handleExecute(message: ClientMessage): Promise { - debug('Execute message:', message); + debug('Execute message:', redactMessage(message)); const { portal, maxRows } = message; @@ -1060,9 +1085,9 @@ export class ProtocolHandler { * Send backend key data */ private sendBackendKeyData(): void { - // Generate random process ID and key - const processId = Math.floor(Math.random() * 10000); - const secretKey = Math.floor(Math.random() * 1000000); + // The secret key authenticates CancelRequest messages, so it must be unguessable. + const processId = randomInt(1, 0x7fffffff); + const secretKey = randomInt(1, 0x7fffffff); this.sendMessage(this.serializer.backendKeyData(processId, secretKey)); } diff --git a/packages/postgres-server/tests/integration/minimal-integration.test.ts b/packages/postgres-server/tests/integration/minimal-integration.test.ts index a0da51b..2b3d200 100644 --- a/packages/postgres-server/tests/integration/minimal-integration.test.ts +++ b/packages/postgres-server/tests/integration/minimal-integration.test.ts @@ -13,7 +13,7 @@ class MockQueryLeaf { constructor(public client: any, public dbName: string) {} execute(query: string) { - log(`Mock executing query: ${query}`); + log(`Mock executing query of length ${query.length}`); if (query.includes('users')) { return [ { name: 'John', age: 30 }, diff --git a/packages/postgres-server/tests/integration/minimal.integration.test.ts b/packages/postgres-server/tests/integration/minimal.integration.test.ts index 49b8268..a4058f9 100644 --- a/packages/postgres-server/tests/integration/minimal.integration.test.ts +++ b/packages/postgres-server/tests/integration/minimal.integration.test.ts @@ -14,7 +14,7 @@ class MockQueryLeaf { constructor(public client: any, public dbName: string) {} execute(query: string): any[] { - log(`Mock executing query: ${query}`); + log(`Mock executing query of length ${query.length}`); if (query.includes('test')) { return [{ test: 'success' }]; } From 698073bb06b6bcd4fa194dfb3c52c7b1fec9cb63 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Fri, 8 May 2026 09:56:06 -0500 Subject: [PATCH 2/2] refactor: share redactSql helper from @queryleaf/lib Pull the credential-masking regex into packages/lib/src/redact.ts and re-export it from the lib's public surface so postgres-server can pull it in instead of carrying its own copy. Also extend the basic.test.ts mock to spread the real module before overriding QueryLeaf, otherwise the new redactSql import resolves to undefined inside the handler under test. --- packages/lib/src/index.ts | 2 ++ packages/lib/src/parser.ts | 10 +--------- packages/lib/src/redact.ts | 10 ++++++++++ packages/postgres-server/src/protocol-handler.ts | 12 +----------- packages/postgres-server/tests/unit/basic.test.ts | 4 +++- 5 files changed, 17 insertions(+), 21 deletions(-) create mode 100644 packages/lib/src/redact.ts diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 7929976..db2b854 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -125,5 +125,7 @@ export { DummyMongoClient, }; +export { redactSql } from './redact'; + // Re-export interfaces export * from './interfaces'; diff --git a/packages/lib/src/parser.ts b/packages/lib/src/parser.ts index 66b9a2f..9c19482 100644 --- a/packages/lib/src/parser.ts +++ b/packages/lib/src/parser.ts @@ -1,18 +1,10 @@ import { From, Parser as NodeSqlParser } from 'node-sql-parser'; import { SqlParser, SqlStatement } from './interfaces'; +import { redactSql } from './redact'; import debug from 'debug'; const rawLog = debug('queryleaf:parser'); -// SQL fed to the parser may contain literal credentials (CREATE USER ... PASSWORD '...'). -// Strip them before they hit debug output. -function redactSql(sql: string): string { - return sql.replace( - /\b(PASSWORD|IDENTIFIED\s+BY|IDENTIFIED\s+WITH\s+\S+\s+AS)\s+('([^']|'')*'|"([^"]|"")*"|\S+)/gi, - '$1 ***' - ); -} - function log(message: string, ...args: unknown[]): void { const safeArgs = args.map((arg) => (typeof arg === 'string' ? redactSql(arg) : arg)); rawLog(message, ...safeArgs); diff --git a/packages/lib/src/redact.ts b/packages/lib/src/redact.ts new file mode 100644 index 0000000..2fca9b3 --- /dev/null +++ b/packages/lib/src/redact.ts @@ -0,0 +1,10 @@ +// SQL fed to the parser or the postgres protocol handler may carry literal +// credentials (CREATE USER ... PASSWORD '...', ALTER USER ... IDENTIFIED BY ...). +// Mask them before they reach debug logs. +export function redactSql(sql: string | undefined): string { + if (!sql) return ''; + return sql.replace( + /\b(PASSWORD|IDENTIFIED\s+BY|IDENTIFIED\s+WITH\s+\S+\s+AS)\s+('([^']|'')*'|"([^"]|"")*"|\S+)/gi, + '$1 ***' + ); +} diff --git a/packages/postgres-server/src/protocol-handler.ts b/packages/postgres-server/src/protocol-handler.ts index 9f2bb37..8e98d44 100644 --- a/packages/postgres-server/src/protocol-handler.ts +++ b/packages/postgres-server/src/protocol-handler.ts @@ -1,5 +1,5 @@ import { Socket } from 'net'; -import { QueryLeaf } from '@queryleaf/lib'; +import { QueryLeaf, redactSql } from '@queryleaf/lib'; import { Transform } from 'stream'; import { randomInt } from 'crypto'; import debugLib from 'debug'; @@ -7,16 +7,6 @@ import { MongoClient, Document } from 'mongodb'; const debug = debugLib('queryleaf:pg-server:protocol'); -// SQL statements may carry literal credentials (e.g., CREATE USER ... PASSWORD '...'). -// Mask them before they reach debug logs. -function redactSql(sql: string | undefined): string { - if (!sql) return ''; - return sql.replace( - /\b(PASSWORD|IDENTIFIED\s+BY|IDENTIFIED\s+WITH\s+\S+\s+AS)\s+('([^']|'')*'|"([^"]|"")*"|\S+)/gi, - '$1 ***' - ); -} - // Strip password / query payloads from a parsed client message before logging. function redactMessage(message: { type?: string; string?: string; query?: string }): object { const { string: _str, query, ...rest } = message; diff --git a/packages/postgres-server/tests/unit/basic.test.ts b/packages/postgres-server/tests/unit/basic.test.ts index 2e1731c..7e9205c 100644 --- a/packages/postgres-server/tests/unit/basic.test.ts +++ b/packages/postgres-server/tests/unit/basic.test.ts @@ -4,10 +4,12 @@ import { Socket } from 'net'; // Mock the QueryLeaf import jest.mock('@queryleaf/lib', () => { + const actual = jest.requireActual('@queryleaf/lib'); return { + ...actual, QueryLeaf: class MockQueryLeaf { constructor(public client: any, public dbName: string) {} - + async execute(sql: string): Promise { if (sql === 'SELECT * FROM users') { return [