Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
SPAN_STATUS_ERROR,
startSpan,
} from '@sentry/core';
import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
filterCollectedDbQueryText,
} from '@sentry/core/server';
import { ensureInstrumented } from '../../instrument';

// Patching is based on internal Cloudflare D1 API
Expand Down Expand Up @@ -134,15 +138,16 @@ function createStartSpanOptions(query: string, type: D1QueryType): StartSpanOpti
const querySummary = query ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(query)) : undefined;

const client = getClient();
const name = client && hasSpanStreamingEnabled(client) ? querySummary || 'cloudflare-d1' : query;
const queryText = filterCollectedDbQueryText(query, undefined, client);
const name = client && hasSpanStreamingEnabled(client) ? querySummary || 'cloudflare-d1' : queryText;

return {
name,
attributes: {
[SENTRY_OP]: DB_QUERY,
'db.system.name': 'cloudflare-d1',
'db.operation.name': type,
'db.query.text': query,
'db.query.text': queryText,
'db.query.summary': querySummary,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe('instrumentD1', () => {
beforeEach(() => {
getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue({
getOptions: () => ({ traceLifecycle: 'stream' }),
getDataCollectionOptions: () => ({ databaseQueryData: true }),
} as unknown as ReturnType<typeof SentryCore.getClient>);
});

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
getSqlQuerySummary as _INTERNAL_getSqlQuerySummary,
sanitizeSqlQuery as _INTERNAL_sanitizeSqlQuery,
} from './utils/sql';
export { filterCollectedDbQueryText } from './utils/data-collection/filterCollectedDbQueryText';
export type { SqlDialect } from './utils/sql';

export { patchHttpModuleClient } from './integrations/http/client-patch';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Client } from '../../client';
import { getClient } from '../../currentScopes';
import type { SqlDialect } from '../sql';
import { sanitizeSqlQuery } from '../sql';

/**
* Applies `dataCollection.databaseQueryData` to a SQL statement the SDK collected itself, for use as
* `db.query.text`.
*
* A statement can carry inline literal values (`WHERE email = 'jane@example.com'`), which the spec
* counts as database query data. Sanitized statements are not gated, so with the option off the
* literals are replaced with `?` rather than the attribute being dropped — the shape of the query
* stays available for debugging.
*
* Pass the `client` the statement belongs to whenever one is at hand; falling back to `getClient()`
* resolves against the current scope, which is the wrong client in a multi-client setup.
*/
export function filterCollectedDbQueryText(query: string, dialect?: SqlDialect, client?: Client): string;
export function filterCollectedDbQueryText(
query: string | undefined,
dialect?: SqlDialect,
client?: Client,
): string | undefined;
export function filterCollectedDbQueryText(
query: string | undefined,
dialect?: SqlDialect,
client?: Client,
): string | undefined {
if (query === undefined) {
return undefined;
}
// Instrumentation can run before a client exists; collecting is the documented default.
const collect = (client ?? getClient())?.getDataCollectionOptions().databaseQueryData !== false;
return collect ? query : sanitizeSqlQuery(query, dialect);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import type { Client } from '../../../../src/client';
import type { DataCollection } from '../../../../src/types/datacollection';
import { filterCollectedDbQueryText } from '../../../../src/utils/data-collection/filterCollectedDbQueryText';
import { resolveDataCollectionOptions } from '../../../../src/utils/data-collection/resolveDataCollectionOptions';

function mockClient(dataCollection?: DataCollection): Client {
return { getDataCollectionOptions: () => resolveDataCollectionOptions({ dataCollection }) } as unknown as Client;
}

describe('filterCollectedDbQueryText', () => {
it('returns undefined for an absent statement', () => {
expect(filterCollectedDbQueryText(undefined, undefined, mockClient())).toBeUndefined();
});

it('keeps inline literals by default', () => {
expect(
filterCollectedDbQueryText("SELECT * FROM users WHERE email = 'jane@example.com'", undefined, mockClient()),
).toBe("SELECT * FROM users WHERE email = 'jane@example.com'");
});

it('sanitizes inline literals when databaseQueryData is off', () => {
expect(
filterCollectedDbQueryText("SELECT * FROM users WHERE email = 'jane@example.com'", undefined, {
...mockClient({ databaseQueryData: false }),
}),
).toBe('SELECT * FROM users WHERE email = ?');
});
});
9 changes: 7 additions & 2 deletions packages/nuxt/src/runtime/utils/instrumentDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
startSpan,
type StartSpanOptions,
} from '@sentry/core';
import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery, flushIfServerless } from '@sentry/core/server';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
filterCollectedDbQueryText,
flushIfServerless,
} from '@sentry/core/server';

Check warning on line 21 in packages/nuxt/src/runtime/utils/instrumentDatabase.ts

View check run for this annotation

@sentry/warden / warden: security-review

Nuxt DB breadcrumbs and static span names bypass databaseQueryData redaction

Nuxt's database instrumentation filters only the `db.query.text` span attribute; successful queries still place the raw SQL in breadcrumb fields and use it as the span name when span streaming is disabled. Inline literals derived from caller or request data can therefore be sent despite `dataCollection.databaseQueryData: false`; apply the helper to the breadcrumb and non-streaming span-name paths.
Comment on lines +19 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nuxt DB breadcrumbs and static span names bypass databaseQueryData redaction

Nuxt's database instrumentation filters only the db.query.text span attribute; successful queries still place the raw SQL in breadcrumb fields and use it as the span name when span streaming is disabled. Inline literals derived from caller or request data can therefore be sent despite dataCollection.databaseQueryData: false; apply the helper to the breadcrumb and non-streaming span-name paths.

Evidence
  • createBreadcrumb() passes the unfiltered query to both message and data['db.query.text']; addBreadcrumb() stores these values on the current isolation scope for subsequent events.
  • In createStartSpanOptions(), the non-streaming branch sets name directly to query, while only the db.query.text attribute uses filterCollectedDbQueryText.
  • Nuxt's production database plugins invoke this instrumentation for configured Nitro databases, and db.exec plus prepared-statement methods accept statements containing inline literals.
  • No central breadcrumb or span-name filtering applies databaseQueryData, so a successful query followed by an error event or a static transaction can expose the raw literal.

Identified by Warden · security-review · ZWT-URM

import type { Database, PreparedStatement } from 'db0';
import { type DatabaseConnectionConfig, type DatabaseSpanData, getDatabaseSpanData } from './database-span-data';
import { DB_NAMESPACE, DB_QUERY_SUMMARY, DB_QUERY_TEXT, DB_SYSTEM_NAME } from '@sentry/conventions/attributes';
Expand Down Expand Up @@ -260,7 +265,7 @@
return {
name,
attributes: {
[DB_QUERY_TEXT]: query,
[DB_QUERY_TEXT]: filterCollectedDbQueryText(query),
[DB_QUERY_SUMMARY]: querySummary,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN,
[SENTRY_OP]: DB_QUERY,
Expand Down
9 changes: 7 additions & 2 deletions packages/server-utils/src/integrations/knex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import { DB } from '@sentry/conventions/op';
import { DEBUG_BUILD } from '../debug-build';
import { CHANNELS } from '../orchestrion/channels';
import { bindTracingChannelToSpan } from '../tracing-channel';
import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
filterCollectedDbQueryText,
} from '@sentry/core/server';

// NOTE: this uses the same name as the OTel integration by design. `@sentry/node`'s `knexIntegration`
// picks this subscriber over the vendored OTel path when orchestrion injection is active.
Expand Down Expand Up @@ -174,8 +178,9 @@ function subscribeQuery(): void {
connection?.filename || connection?.database || extractDatabaseFromConnectionString(connectionString);
const dbSystem = mapSystem(client?.driverName);

const dbStatement = query?.sql != null ? truncate(query.sql, MAX_QUERY_LENGTH) : undefined;
const dialect = client?.driverName === 'mysql' || client?.driverName === 'mysql2' ? 'mysql' : undefined;
const dbStatement =
query?.sql != null ? filterCollectedDbQueryText(truncate(query.sql, MAX_QUERY_LENGTH), dialect) : undefined;
const querySummary = dbStatement
? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(dbStatement, dialect))
: undefined;
Expand Down
11 changes: 8 additions & 3 deletions packages/server-utils/src/integrations/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
} from '@sentry/core';
import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
filterCollectedDbQueryText,
} from '@sentry/core/server';
import { CHANNELS } from '../orchestrion/channels';
import { bindTracingChannelToSpan } from '../tracing-channel';
import { mysqlModuleNames } from '../orchestrion/config/mysql';
Expand Down Expand Up @@ -91,10 +95,11 @@ function instrumentMysql(): void {
const querySummary = sql ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(sql, 'mysql')) : undefined;

const client = getClient();
const queryText = filterCollectedDbQueryText(sql, 'mysql', client);
const name =
client && hasSpanStreamingEnabled(client)
? querySummary || database || DB_SYSTEM_NAME_VALUE_MYSQL
: (sql ?? 'mysql.query');
: (queryText ?? 'mysql.query');

return startInactiveSpan({
name,
Expand All @@ -106,7 +111,7 @@ function instrumentMysql(): void {
[ATTR_DB_CONNECTION_STRING]: getJDBCString(host, portIsNumber ? portNumber : undefined, database),
...(database ? { [DB_NAMESPACE]: database } : {}),
...(user ? { [DB_USER]: user } : {}),
...(sql ? { [DB_QUERY_TEXT]: sql } : {}),
...(queryText ? { [DB_QUERY_TEXT]: queryText } : {}),
[DB_QUERY_SUMMARY]: querySummary,
[SERVER_ADDRESS]: host,
[SERVER_PORT]: portIsNumber ? portNumber : undefined,
Expand Down
11 changes: 8 additions & 3 deletions packages/server-utils/src/integrations/mysql2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
startInactiveSpan,
waitForTracingChannelBinding,
} from '@sentry/core';
import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
filterCollectedDbQueryText,
} from '@sentry/core/server';
import { subscribeMysql2DiagnosticChannels } from './mysql2-dc-subscriber';
import type { ChannelName } from '../../orchestrion/channels';
import { CHANNELS } from '../../orchestrion/channels';
Expand Down Expand Up @@ -89,10 +93,11 @@ function subscribeQueryChannel(channelName: ChannelName): void {
: undefined;

const client = getClient();
const queryText = filterCollectedDbQueryText(statement, 'mysql', client);
const name =
client && hasSpanStreamingEnabled(client)
? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL
: (statement ?? 'mysql2.query');
: (queryText ?? 'mysql2.query');

return startInactiveSpan({
name,
Expand All @@ -101,7 +106,7 @@ function subscribeQueryChannel(channelName: ChannelName): void {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
[SENTRY_OP]: DB,
[DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL,
[DB_QUERY_TEXT]: statement || undefined,
[DB_QUERY_TEXT]: queryText || undefined,
[DB_QUERY_SUMMARY]: querySummary,
...connectionAttributes,
},
Expand Down
11 changes: 8 additions & 3 deletions packages/server-utils/src/integrations/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
} from '@sentry/core';
import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
filterCollectedDbQueryText,
} from '@sentry/core/server';
import { CHANNELS } from '../orchestrion/channels';
import { bindTracingChannelToSpan } from '../tracing-channel';
import { pgModuleNames } from '../orchestrion/config/pg';
Expand Down Expand Up @@ -186,18 +190,19 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp
? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(queryConfig.text))
: undefined;

const queryText = filterCollectedDbQueryText(queryConfig?.text, undefined, client);
const name =
client && hasSpanStreamingEnabled(client)
? querySummary || params.database || DB_SYSTEM_POSTGRESQL
: (queryConfig?.text ?? SPAN_QUERY_FALLBACK);
: (queryText ?? SPAN_QUERY_FALLBACK);

return {
name,
attributes: {
[SENTRY_OP]: DB,
...getConnectionAttributes(params),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
[DB_QUERY_TEXT]: queryConfig?.text || undefined,
[DB_QUERY_TEXT]: queryText || undefined,
[DB_QUERY_SUMMARY]: querySummary,
[ATTR_PG_PLAN]: typeof queryConfig?.name === 'string' ? queryConfig.name : undefined,
},
Expand Down
8 changes: 6 additions & 2 deletions packages/server-utils/src/integrations/tedious.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import { DB } from '@sentry/conventions/op';
import { CHANNELS } from '../orchestrion/channels';
import { tediousModuleNames } from '../orchestrion/config/tedious';
import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation';
import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
filterCollectedDbQueryText,
} from '@sentry/core/server';

// NOTE: this uses the same name as the OTel integration by design. When orchestrion injection is active,
// `_init` swaps the OTel `Tedious` integration out of the defaults and appends this one (matched by name).
Expand Down Expand Up @@ -143,7 +147,7 @@ function subscribeQuery(channelName: string, operation: string): void {
[DB_NAMESPACE]: databaseName,
// `>=4` uses the `authentication` object; older versions expose `userName` directly.
[DB_USER]: connection.config?.userName ?? connection.config?.authentication?.options?.userName,
[DB_QUERY_TEXT]: sql,
[DB_QUERY_TEXT]: filterCollectedDbQueryText(sql),
[DB_QUERY_SUMMARY]: querySummary,
[ATTR_DB_SQL_TABLE]: request.table,
[SERVER_ADDRESS]: connection.config?.server,
Expand Down
Loading