Skip to content

Commit 1b0fb52

Browse files
committed
fix(google-ads): add input validation for GAQL query parameters
1 parent 093cfac commit 1b0fb52

6 files changed

Lines changed: 87 additions & 20 deletions

File tree

apps/sim/tools/google_ads/ad_performance.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
GoogleAdsAdPerformanceParams,
33
GoogleAdsAdPerformanceResponse,
44
} from '@/tools/google_ads/types'
5+
import { validateDate, validateDateRange, validateNumericId } from '@/tools/google_ads/types'
56
import type { ToolConfig } from '@/tools/types'
67

78
export const googleAdsAdPerformanceTool: ToolConfig<
@@ -83,8 +84,10 @@ export const googleAdsAdPerformanceTool: ToolConfig<
8384
},
8485

8586
request: {
86-
url: (params) =>
87-
`https://googleads.googleapis.com/v19/customers/${params.customerId}/googleAds:search`,
87+
url: (params) => {
88+
const customerId = validateNumericId(params.customerId, 'customerId')
89+
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
90+
},
8891
method: 'POST',
8992
headers: (params) => {
9093
const headers: Record<string, string> = {
@@ -104,17 +107,19 @@ export const googleAdsAdPerformanceTool: ToolConfig<
104107
const conditions: string[] = ["ad_group_ad.status != 'REMOVED'"]
105108

106109
if (params.campaignId) {
107-
conditions.push(`campaign.id = ${params.campaignId}`)
110+
conditions.push(`campaign.id = ${validateNumericId(params.campaignId, 'campaignId')}`)
108111
}
109112

110113
if (params.adGroupId) {
111-
conditions.push(`ad_group.id = ${params.adGroupId}`)
114+
conditions.push(`ad_group.id = ${validateNumericId(params.adGroupId, 'adGroupId')}`)
112115
}
113116

114117
if (params.startDate && params.endDate) {
115-
conditions.push(`segments.date BETWEEN '${params.startDate}' AND '${params.endDate}'`)
118+
const start = validateDate(params.startDate, 'startDate')
119+
const end = validateDate(params.endDate, 'endDate')
120+
conditions.push(`segments.date BETWEEN '${start}' AND '${end}'`)
116121
} else {
117-
const dateRange = params.dateRange || 'LAST_30_DAYS'
122+
const dateRange = validateDateRange(params.dateRange || 'LAST_30_DAYS')
118123
conditions.push(`segments.date DURING ${dateRange}`)
119124
}
120125

apps/sim/tools/google_ads/campaign_performance.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
GoogleAdsCampaignPerformanceParams,
33
GoogleAdsCampaignPerformanceResponse,
44
} from '@/tools/google_ads/types'
5+
import { validateDate, validateDateRange, validateNumericId } from '@/tools/google_ads/types'
56
import type { ToolConfig } from '@/tools/types'
67

78
export const googleAdsCampaignPerformanceTool: ToolConfig<
@@ -71,8 +72,10 @@ export const googleAdsCampaignPerformanceTool: ToolConfig<
7172
},
7273

7374
request: {
74-
url: (params) =>
75-
`https://googleads.googleapis.com/v19/customers/${params.customerId}/googleAds:search`,
75+
url: (params) => {
76+
const customerId = validateNumericId(params.customerId, 'customerId')
77+
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
78+
},
7679
method: 'POST',
7780
headers: (params) => {
7881
const headers: Record<string, string> = {
@@ -92,13 +95,15 @@ export const googleAdsCampaignPerformanceTool: ToolConfig<
9295
const conditions: string[] = ["campaign.status != 'REMOVED'"]
9396

9497
if (params.campaignId) {
95-
conditions.push(`campaign.id = ${params.campaignId}`)
98+
conditions.push(`campaign.id = ${validateNumericId(params.campaignId, 'campaignId')}`)
9699
}
97100

98101
if (params.startDate && params.endDate) {
99-
conditions.push(`segments.date BETWEEN '${params.startDate}' AND '${params.endDate}'`)
102+
const start = validateDate(params.startDate, 'startDate')
103+
const end = validateDate(params.endDate, 'endDate')
104+
conditions.push(`segments.date BETWEEN '${start}' AND '${end}'`)
100105
} else {
101-
const dateRange = params.dateRange || 'LAST_30_DAYS'
106+
const dateRange = validateDateRange(params.dateRange || 'LAST_30_DAYS')
102107
conditions.push(`segments.date DURING ${dateRange}`)
103108
}
104109

apps/sim/tools/google_ads/list_ad_groups.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
GoogleAdsListAdGroupsParams,
33
GoogleAdsListAdGroupsResponse,
44
} from '@/tools/google_ads/types'
5+
import { validateNumericId, validateStatus } from '@/tools/google_ads/types'
56
import type { ToolConfig } from '@/tools/types'
67

78
export const googleAdsListAdGroupsTool: ToolConfig<
@@ -64,8 +65,10 @@ export const googleAdsListAdGroupsTool: ToolConfig<
6465
},
6566

6667
request: {
67-
url: (params) =>
68-
`https://googleads.googleapis.com/v19/customers/${params.customerId}/googleAds:search`,
68+
url: (params) => {
69+
const customerId = validateNumericId(params.customerId, 'customerId')
70+
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
71+
},
6972
method: 'POST',
7073
headers: (params) => {
7174
const headers: Record<string, string> = {
@@ -82,10 +85,11 @@ export const googleAdsListAdGroupsTool: ToolConfig<
8285
let query =
8386
'SELECT ad_group.id, ad_group.name, ad_group.status, ad_group.type, campaign.id, campaign.name FROM ad_group'
8487

85-
const conditions: string[] = [`campaign.id = ${params.campaignId}`]
88+
const campaignId = validateNumericId(params.campaignId, 'campaignId')
89+
const conditions: string[] = [`campaign.id = ${campaignId}`]
8690

8791
if (params.status) {
88-
conditions.push(`ad_group.status = '${params.status}'`)
92+
conditions.push(`ad_group.status = '${validateStatus(params.status)}'`)
8993
} else {
9094
conditions.push("ad_group.status != 'REMOVED'")
9195
}

apps/sim/tools/google_ads/list_campaigns.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
GoogleAdsListCampaignsParams,
33
GoogleAdsListCampaignsResponse,
44
} from '@/tools/google_ads/types'
5+
import { validateNumericId, validateStatus } from '@/tools/google_ads/types'
56
import type { ToolConfig } from '@/tools/types'
67

78
export const googleAdsListCampaignsTool: ToolConfig<
@@ -58,8 +59,10 @@ export const googleAdsListCampaignsTool: ToolConfig<
5859
},
5960

6061
request: {
61-
url: (params) =>
62-
`https://googleads.googleapis.com/v19/customers/${params.customerId}/googleAds:search`,
62+
url: (params) => {
63+
const customerId = validateNumericId(params.customerId, 'customerId')
64+
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
65+
},
6366
method: 'POST',
6467
headers: (params) => {
6568
const headers: Record<string, string> = {
@@ -78,7 +81,7 @@ export const googleAdsListCampaignsTool: ToolConfig<
7881

7982
const conditions: string[] = []
8083
if (params.status) {
81-
conditions.push(`campaign.status = '${params.status}'`)
84+
conditions.push(`campaign.status = '${validateStatus(params.status)}'`)
8285
} else {
8386
conditions.push("campaign.status != 'REMOVED'")
8487
}

apps/sim/tools/google_ads/search.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { GoogleAdsSearchParams, GoogleAdsSearchResponse } from '@/tools/google_ads/types'
2+
import { validateNumericId } from '@/tools/google_ads/types'
23
import type { ToolConfig } from '@/tools/types'
34

45
export const googleAdsSearchTool: ToolConfig<GoogleAdsSearchParams, GoogleAdsSearchResponse> = {
@@ -58,8 +59,10 @@ export const googleAdsSearchTool: ToolConfig<GoogleAdsSearchParams, GoogleAdsSea
5859
},
5960

6061
request: {
61-
url: (params) =>
62-
`https://googleads.googleapis.com/v19/customers/${params.customerId}/googleAds:search`,
62+
url: (params) => {
63+
const customerId = validateNumericId(params.customerId, 'customerId')
64+
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
65+
},
6366
method: 'POST',
6467
headers: (params) => {
6568
const headers: Record<string, string> = {

apps/sim/tools/google_ads/types.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
import type { ToolResponse } from '@/tools/types'
22

3+
const NUMERIC_ID_REGEX = /^\d+$/
4+
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/
5+
const VALID_STATUSES = new Set(['ENABLED', 'PAUSED', 'REMOVED'])
6+
const VALID_DATE_RANGES = new Set([
7+
'LAST_7_DAYS',
8+
'LAST_30_DAYS',
9+
'THIS_MONTH',
10+
'LAST_MONTH',
11+
'TODAY',
12+
'YESTERDAY',
13+
])
14+
15+
/** Validates that a value is a numeric ID (digits only). */
16+
export function validateNumericId(value: string, fieldName: string): string {
17+
const cleaned = value.replace(/-/g, '')
18+
if (!NUMERIC_ID_REGEX.test(cleaned)) {
19+
throw new Error(`${fieldName} must be numeric (digits only), got: ${value}`)
20+
}
21+
return cleaned
22+
}
23+
24+
/** Validates that a status value is a known Google Ads status. */
25+
export function validateStatus(value: string): string {
26+
if (!VALID_STATUSES.has(value)) {
27+
throw new Error(`Invalid status: ${value}. Must be one of: ${[...VALID_STATUSES].join(', ')}`)
28+
}
29+
return value
30+
}
31+
32+
/** Validates a date string is in YYYY-MM-DD format. */
33+
export function validateDate(value: string, fieldName: string): string {
34+
if (!DATE_REGEX.test(value)) {
35+
throw new Error(`${fieldName} must be in YYYY-MM-DD format, got: ${value}`)
36+
}
37+
return value
38+
}
39+
40+
/** Validates a date range is a known Google Ads predefined range. */
41+
export function validateDateRange(value: string): string {
42+
if (!VALID_DATE_RANGES.has(value)) {
43+
throw new Error(
44+
`Invalid date range: ${value}. Must be one of: ${[...VALID_DATE_RANGES].join(', ')}`
45+
)
46+
}
47+
return value
48+
}
49+
350
export interface GoogleAdsBaseParams {
451
accessToken: string
552
customerId: string

0 commit comments

Comments
 (0)