Skip to content

Commit 0c9882f

Browse files
jasonmp85claude
andauthored
[sqlserver] Use parameterized queries in AO and fragmentation metrics (#23426)
* [sqlserver] Use parameterized queries in AO and fragmentation metrics Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [sqlserver] Fix hardcoded FreeTDS version in get_local_driver Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f110c25 commit 0c9882f

9 files changed

Lines changed: 159 additions & 23 deletions

File tree

sqlserver/changelog.d/23426.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Escape single quotes in SQL string literal interpolation for robustness against unusual database and object names.

sqlserver/datadog_checks/sqlserver/database_metrics/availability_groups_metrics.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def enabled(self):
5252
def queries(self):
5353
query = AVAILABILITY_GROUPS_METRICS_QUERY.copy()
5454
if self.availability_group:
55-
query['query'] += f" where resource_group_id = '{self.availability_group}'"
55+
query['query'] += " where resource_group_id = ?"
56+
query['params'] = (self.availability_group,)
5657
return [query]
5758

5859
def __repr__(self) -> str:

sqlserver/datadog_checks/sqlserver/database_metrics/availability_replicas_metrics.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,17 @@ def queries(self):
7272
query = AVAILABILITY_REPLICAS_METRICS_QUERY.copy()
7373
if self.availability_group or self.only_emit_local or self.ao_database:
7474
where_clauses = []
75+
params = []
7576
if self.availability_group:
76-
where_clauses.append(f"resource_group_id = '{self.availability_group}'")
77+
where_clauses.append("resource_group_id = ?")
78+
params.append(self.availability_group)
7779
if self.only_emit_local:
7880
where_clauses.append("is_local = 1")
7981
if self.ao_database:
80-
where_clauses.append(f"database_name = '{self.ao_database}'")
82+
where_clauses.append("database_name = ?")
83+
params.append(self.ao_database)
8184
query['query'] += f" where {' and '.join(where_clauses)}"
85+
query['params'] = tuple(params)
8286
if self.major_version >= 12:
8387
# This column only supported in SQL Server 2014 and later
8488
is_primary_replica = "is_primary_replica"

sqlserver/datadog_checks/sqlserver/database_metrics/database_replication_stats_metrics.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,14 @@ def queries(self):
5353
query = DATABASE_REPLICATION_STATS_METRICS_QUERY.copy()
5454
if self.availability_group or self.only_emit_local:
5555
where_clauses = []
56+
params = []
5657
if self.availability_group:
57-
where_clauses.append(f"resource_group_id = '{self.availability_group}'")
58+
where_clauses.append("resource_group_id = ?")
59+
params.append(self.availability_group)
5860
if self.only_emit_local:
5961
where_clauses.append("is_local = 1")
6062
query['query'] += f" where {' and '.join(where_clauses)}"
63+
query['params'] = tuple(params)
6164
return [query]
6265

6366
def __repr__(self) -> str:

sqlserver/datadog_checks/sqlserver/database_metrics/db_fragmentation_metrics.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
DDIPS.avg_fragment_size_in_pages as avg_fragment_size_in_pages,
2222
DDIPS.page_count as page_count,
2323
DDIPS.avg_fragmentation_in_percent as avg_fragmentation_in_percent
24-
FROM sys.dm_db_index_physical_stats (DB_ID('{db}'),null,null,null,null) as DDIPS
24+
FROM sys.dm_db_index_physical_stats (DB_ID(?),null,null,null,null) as DDIPS
2525
INNER JOIN sys.indexes as I WITH (NOLOCK) ON I.object_id = DDIPS.object_id
2626
AND DDIPS.index_id = I.index_id
2727
WHERE DDIPS.fragment_count is not null
@@ -112,14 +112,19 @@ def __repr__(self) -> str:
112112

113113
def _build_query_executors(self):
114114
executors = []
115+
if self.db_fragmentation_object_names:
116+
placeholders = ','.join(['?'] * len(self.db_fragmentation_object_names))
117+
object_name_filter = f" AND OBJECT_NAME(DDIPS.object_id, DDIPS.database_id) IN ({placeholders})"
118+
else:
119+
object_name_filter = None
115120
for database in self.databases:
116121
queries = copy.deepcopy(self.queries)
117122
for query in queries:
118-
query['query'] = query['query'].format(db=database)
119-
if self.db_fragmentation_object_names:
120-
query['query'] += " AND OBJECT_NAME(DDIPS.object_id, DDIPS.database_id) IN ({})".format(
121-
','.join(["'{}'".format(name) for name in self.db_fragmentation_object_names])
122-
)
123+
params = [database]
124+
if object_name_filter:
125+
query['query'] += object_name_filter
126+
params.extend(self.db_fragmentation_object_names)
127+
query['params'] = tuple(params)
123128
executor = self.new_query_executor(
124129
queries,
125130
executor=functools.partial(self.execute_query_handler, db=database),

sqlserver/datadog_checks/sqlserver/sqlserver.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,13 +1064,16 @@ def collect_metrics(self):
10641064
with self.connection.get_managed_cursor(KEY_PREFIX) as cursor:
10651065
cursor.execute("SET NOCOUNT OFF")
10661066

1067-
def execute_query_raw(self, query, db=None):
1067+
def execute_query_raw(self, query, db=None, params=None):
10681068
with self.connection.get_managed_cursor(KEY_PREFIX) as cursor:
10691069
if db:
10701070
ctx = construct_use_statement(db)
10711071
self.log.debug("changing cursor context via use statement: %s", ctx)
10721072
cursor.execute(ctx)
1073-
cursor.execute(query)
1073+
if params is not None:
1074+
cursor.execute(query, params)
1075+
else:
1076+
cursor.execute(query)
10741077
return cursor.fetchall()
10751078

10761079
def do_stored_procedure_check(self):

sqlserver/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ classifiers = [
2828
"Private :: Do Not Upload",
2929
]
3030
dependencies = [
31-
"datadog-checks-base>=37.34.1",
31+
"datadog-checks-base>=37.36.0",
3232
]
3333
dynamic = [
3434
"version",

sqlserver/tests/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def get_local_driver():
3737
we need to define the 'FreeTDS' driver in odbcinst.ini
3838
"""
3939
if ON_MACOS:
40-
return '/opt/homebrew/Cellar/freetds/1.4.26/lib/libtdsodbc.so'
40+
return '/opt/homebrew/lib/libtdsodbc.so'
4141
elif ON_WINDOWS:
4242
return '{ODBC Driver 18 for SQL Server}'
4343
else:

sqlserver/tests/test_database_metrics.py

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,59 @@ def test_sqlserver_ao_metrics(
255255
aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags)
256256

257257

258+
def test_sqlserver_availability_groups_query_uses_parameterized_queries(init_config, instance_docker_metrics):
259+
ag_with_quotes = "AG1'; DROP TABLE foo; --"
260+
instance_docker_metrics['database_metrics'] = {
261+
'ao_metrics': {'enabled': True, 'availability_group': ag_with_quotes},
262+
}
263+
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
264+
metrics = SqlserverAvailabilityGroupsMetrics(
265+
config=sqlserver_check._config,
266+
new_query_executor=sqlserver_check._new_query_executor,
267+
server_static_info=STATIC_SERVER_INFO,
268+
execute_query_handler=mock.MagicMock(),
269+
)
270+
q = metrics.queries[0]
271+
assert "resource_group_id = ?" in q['query']
272+
assert q['params'] == (ag_with_quotes,)
273+
274+
275+
def test_sqlserver_database_replication_stats_query_uses_parameterized_queries(init_config, instance_docker_metrics):
276+
ag_with_quotes = "AG1'; DROP TABLE foo; --"
277+
instance_docker_metrics['database_metrics'] = {
278+
'ao_metrics': {'enabled': True, 'availability_group': ag_with_quotes},
279+
}
280+
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
281+
metrics = SqlserverDatabaseReplicationStatsMetrics(
282+
config=sqlserver_check._config,
283+
new_query_executor=sqlserver_check._new_query_executor,
284+
server_static_info=STATIC_SERVER_INFO,
285+
execute_query_handler=mock.MagicMock(),
286+
)
287+
q = metrics.queries[0]
288+
assert "resource_group_id = ?" in q['query']
289+
assert q['params'] == (ag_with_quotes,)
290+
291+
292+
def test_sqlserver_availability_replicas_query_uses_parameterized_queries(init_config, instance_docker_metrics):
293+
ag_with_quotes = "AG1'; DROP TABLE foo; --"
294+
db_with_quotes = "mydb'; DROP TABLE bar; --"
295+
instance_docker_metrics['database_metrics'] = {
296+
'ao_metrics': {'enabled': True, 'availability_group': ag_with_quotes, 'ao_database': db_with_quotes},
297+
}
298+
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
299+
metrics = SqlserverAvailabilityReplicasMetrics(
300+
config=sqlserver_check._config,
301+
new_query_executor=sqlserver_check._new_query_executor,
302+
server_static_info=STATIC_SERVER_INFO,
303+
execute_query_handler=mock.MagicMock(),
304+
)
305+
q = metrics.queries[0]
306+
assert "resource_group_id = ?" in q['query']
307+
assert "database_name = ?" in q['query']
308+
assert q['params'] == (ag_with_quotes, db_with_quotes)
309+
310+
258311
@pytest.mark.integration
259312
@pytest.mark.usefixtures('dd_environment')
260313
@pytest.mark.parametrize('include_ao_metrics', [True, False])
@@ -287,7 +340,7 @@ def test_sqlserver_availability_groups_metrics(
287340

288341
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
289342

290-
def execute_query_handler_mocked(query, db=None):
343+
def execute_query_handler_mocked(query, db=None, params=None):
291344
return mocked_results
292345

293346
availability_groups_metrics = SqlserverAvailabilityGroupsMetrics(
@@ -298,9 +351,9 @@ def execute_query_handler_mocked(query, db=None):
298351
)
299352

300353
if availability_group:
301-
assert availability_groups_metrics.queries[0]['query'].endswith(
302-
f" where resource_group_id = '{availability_group}'"
303-
)
354+
q = availability_groups_metrics.queries[0]
355+
assert "resource_group_id = ?" in q['query']
356+
assert q.get('params') == (availability_group,)
304357

305358
sqlserver_check._database_metrics = [availability_groups_metrics]
306359

@@ -389,7 +442,7 @@ def test_sqlserver_database_replication_stats_metrics(
389442

390443
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
391444

392-
def execute_query_handler_mocked(query, db=None):
445+
def execute_query_handler_mocked(query, db=None, params=None):
393446
return mocked_results
394447

395448
database_replication_stats_metrics = SqlserverDatabaseReplicationStatsMetrics(
@@ -400,7 +453,8 @@ def execute_query_handler_mocked(query, db=None):
400453
)
401454

402455
if availability_group:
403-
assert f"resource_group_id = '{availability_group}'" in database_replication_stats_metrics.queries[0]['query']
456+
assert "resource_group_id = ?" in database_replication_stats_metrics.queries[0]['query']
457+
assert availability_group in database_replication_stats_metrics.queries[0].get('params', ())
404458
if only_emit_local:
405459
assert "is_local = 1" in database_replication_stats_metrics.queries[0]['query']
406460

@@ -530,7 +584,7 @@ def test_sqlserver_availability_replicas_metrics(
530584

531585
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
532586

533-
def execute_query_handler_mocked(query, db=None):
587+
def execute_query_handler_mocked(query, db=None, params=None):
534588
return mocked_results
535589

536590
availability_replicas_metrics = SqlserverAvailabilityReplicasMetrics(
@@ -541,11 +595,13 @@ def execute_query_handler_mocked(query, db=None):
541595
)
542596

543597
if availability_group:
544-
assert f"resource_group_id = '{availability_group}'" in availability_replicas_metrics.queries[0]['query']
598+
assert "resource_group_id = ?" in availability_replicas_metrics.queries[0]['query']
599+
assert availability_group in availability_replicas_metrics.queries[0].get('params', ())
545600
if only_emit_local:
546601
assert "is_local = 1" in availability_replicas_metrics.queries[0]['query']
547602
if ao_database:
548-
assert f"database_name = '{ao_database}'" in availability_replicas_metrics.queries[0]['query']
603+
assert "database_name = ?" in availability_replicas_metrics.queries[0]['query']
604+
assert ao_database in availability_replicas_metrics.queries[0].get('params', ())
549605

550606
sqlserver_check._database_metrics = [availability_replicas_metrics]
551607

@@ -1024,6 +1080,69 @@ def test_sqlserver_index_usage_metrics(
10241080
aggregator.assert_metric(metric_name, count=0)
10251081

10261082

1083+
def test_sqlserver_db_fragmentation_query_uses_parameterized_queries(init_config, instance_docker_metrics):
1084+
instance_docker_metrics['database_autodiscovery'] = True
1085+
instance_docker_metrics['database_metrics'] = {
1086+
'db_fragmentation_metrics': {'enabled': True, 'enabled_tempdb': False},
1087+
}
1088+
db_with_quotes = "legit_db'; DROP TABLE foo; --"
1089+
1090+
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
1091+
1092+
captured_queries = []
1093+
1094+
def capture_new_query_executor(queries, **kwargs):
1095+
captured_queries.extend(queries)
1096+
return mock.MagicMock()
1097+
1098+
db_fragmentation_metrics = SqlserverDBFragmentationMetrics(
1099+
config=sqlserver_check._config,
1100+
new_query_executor=capture_new_query_executor,
1101+
server_static_info=STATIC_SERVER_INFO,
1102+
execute_query_handler=mock.MagicMock(),
1103+
databases=[db_with_quotes],
1104+
)
1105+
1106+
db_fragmentation_metrics._build_query_executors()
1107+
1108+
assert len(captured_queries) == 1
1109+
q = captured_queries[0]
1110+
assert "DB_ID(?)" in q['query']
1111+
assert q['params'] == (db_with_quotes,)
1112+
1113+
1114+
def test_sqlserver_db_fragmentation_object_names_uses_parameterized_queries(init_config, instance_docker_metrics):
1115+
obj_with_quotes = "my_obj'; DROP TABLE foo; --"
1116+
instance_docker_metrics['database_autodiscovery'] = True
1117+
instance_docker_metrics['database_metrics'] = {
1118+
'db_fragmentation_metrics': {'enabled': True, 'enabled_tempdb': False},
1119+
}
1120+
instance_docker_metrics['db_fragmentation_object_names'] = [obj_with_quotes]
1121+
1122+
sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics])
1123+
1124+
captured_queries = []
1125+
1126+
def capture_new_query_executor(queries, **kwargs):
1127+
captured_queries.extend(queries)
1128+
return mock.MagicMock()
1129+
1130+
db_fragmentation_metrics = SqlserverDBFragmentationMetrics(
1131+
config=sqlserver_check._config,
1132+
new_query_executor=capture_new_query_executor,
1133+
server_static_info=STATIC_SERVER_INFO,
1134+
execute_query_handler=mock.MagicMock(),
1135+
databases=['master'],
1136+
)
1137+
1138+
db_fragmentation_metrics._build_query_executors()
1139+
1140+
assert len(captured_queries) == 1
1141+
q = captured_queries[0]
1142+
assert "IN (?)" in q['query']
1143+
assert q['params'] == ('master', obj_with_quotes)
1144+
1145+
10271146
@pytest.mark.integration
10281147
@pytest.mark.usefixtures('dd_environment')
10291148
@pytest.mark.parametrize('include_db_fragmentation_metrics', [True, False])

0 commit comments

Comments
 (0)