diff --git a/cli/templates/integrations/salesforce/files/lib/salesforce-client.ts b/cli/templates/integrations/salesforce/files/lib/salesforce-client.ts index 7b7e49034d..32202c252d 100644 --- a/cli/templates/integrations/salesforce/files/lib/salesforce-client.ts +++ b/cli/templates/integrations/salesforce/files/lib/salesforce-client.ts @@ -95,6 +95,27 @@ interface SalesforceLead { [key: string]: any; } +/** Validate a Salesforce record ID (15 or 18 character alphanumeric). */ +function validateSalesforceId(id: string, label: string): string { + if (!/^[a-zA-Z0-9]{15,18}$/.test(id)) { + throw new Error(`Invalid ${label}: must be a 15 or 18 character Salesforce ID`); + } + return id; +} + +/** Escape a string value for use in SOQL single-quoted literals. */ +function escapeSoql(value: string): string { + return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); +} + +/** Validate a SOQL field name. */ +function validateFieldName(field: string): string { + if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(field)) { + throw new Error(`Invalid SOQL field name: ${field}`); + } + return field; +} + async function salesforceFetch(endpoint: string, options: RequestInit = {}): Promise { const token = await getAccessToken(); if (!token) { @@ -141,6 +162,7 @@ function buildListSoql(params: { }): string { const { object, fields, where, limit, offset } = params; + fields.forEach((f) => validateFieldName(f)); let soql = `SELECT ${fields.join(", ")} FROM ${object}`; if (where) soql += ` WHERE ${where}`; soql += ` ORDER BY LastModifiedDate DESC LIMIT ${limit} OFFSET ${offset}`; @@ -155,6 +177,8 @@ async function getSingleRecord(params: { notFoundMessage: string; }): Promise { const { object, id, fields, notFoundMessage } = params; + fields.forEach((f) => validateFieldName(f)); + validateSalesforceId(id, `${object} ID`); const soql = `SELECT ${fields.join(", ")} FROM ${object} WHERE Id = '${id}'`; const result = await query(soql); @@ -270,7 +294,9 @@ export function listContacts(options?: { "LastModifiedDate", ]; - const where = options?.accountId ? `AccountId = '${options.accountId}'` : undefined; + const where = options?.accountId + ? (validateSalesforceId(options.accountId, "accountId"), `AccountId = '${options.accountId}'`) + : undefined; return query(buildListSoql({ object: "Contact", fields, where, limit, offset })); } @@ -356,7 +382,9 @@ export function listOpportunities(options?: { "LastModifiedDate", ]; - const where = options?.accountId ? `AccountId = '${options.accountId}'` : undefined; + const where = options?.accountId + ? (validateSalesforceId(options.accountId, "accountId"), `AccountId = '${options.accountId}'`) + : undefined; return query( buildListSoql({ object: "Opportunity", fields, where, limit, offset }), @@ -441,7 +469,7 @@ export function listLeads(options?: { "LastModifiedDate", ]; - const where = options?.status ? `Status = '${options.status}'` : undefined; + const where = options?.status ? `Status = '${escapeSoql(options.status)}'` : undefined; return query(buildListSoql({ object: "Lead", fields, where, limit, offset })); } diff --git a/cli/templates/integrations/snowflake/files/lib/snowflake-client.ts b/cli/templates/integrations/snowflake/files/lib/snowflake-client.ts index 911ade21f5..bace1c1de1 100644 --- a/cli/templates/integrations/snowflake/files/lib/snowflake-client.ts +++ b/cli/templates/integrations/snowflake/files/lib/snowflake-client.ts @@ -99,6 +99,16 @@ interface SnowflakeError extends Error { sqlState?: string; } +/** Validate a Snowflake identifier (database, schema, or table name). */ +function validateIdentifier(value: string, label: string): string { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) { + throw new Error( + `Invalid ${label}: must start with a letter or underscore and contain only letters, numbers, and underscores`, + ); + } + return value; +} + async function snowflakeFetch( endpoint: string, options: RequestInit = {}, @@ -241,6 +251,7 @@ export async function listDatabases(): Promise { } export async function listSchemas(database: string): Promise { + validateIdentifier(database, "database name"); const result = await runQuery(`SHOW SCHEMAS IN DATABASE ${database}`); return result.rows as SchemaInfo[]; } @@ -249,6 +260,8 @@ export async function listTables( database: string, schema: string, ): Promise { + validateIdentifier(database, "database name"); + validateIdentifier(schema, "schema name"); const result = await runQuery(`SHOW TABLES IN ${database}.${schema}`); return result.rows as TableInfo[]; } @@ -261,6 +274,9 @@ export async function describeTable( columns: ColumnInfo[]; primaryKeys: string[]; }> { + validateIdentifier(database, "database name"); + validateIdentifier(schema, "schema name"); + validateIdentifier(table, "table name"); const result = await runQuery(`DESCRIBE TABLE ${database}.${schema}.${table}`); const columns = result.rows as ColumnInfo[]; @@ -276,6 +292,9 @@ export async function getTableRowCount( schema: string, table: string, ): Promise { + validateIdentifier(database, "database name"); + validateIdentifier(schema, "schema name"); + validateIdentifier(table, "table name"); const result = await runQuery( `SELECT COUNT(*) as count FROM ${database}.${schema}.${table}`, );