@@ -133,6 +133,14 @@ class SalesforceCRMService implements CRM {
133133 private credentialId : number ;
134134 private describeCache = new Map < string , Set < string > > ( ) ;
135135
136+ /**
137+ * Escapes a string value for safe interpolation into SOQL queries.
138+ * Prevents SOQL injection by escaping backslashes and single quotes.
139+ */
140+ private sanitizeSoqlValue ( value : string ) : string {
141+ return value . replace ( / \\ / g, "\\\\" ) . replace ( / ' / g, "\\'" ) ;
142+ }
143+
136144 constructor ( credential : CredentialPayload , appOptions : z . infer < typeof appDataSchema > , testMode = false ) {
137145 this . integrationName = "salesforce_other_calendar" ;
138146 this . credentialId = credential . id ;
@@ -289,7 +297,7 @@ class SalesforceCRMService implements CRM {
289297 private getSalesforceUserIdFromEmail = async ( email : string ) => {
290298 const conn = await this . conn ;
291299 const query = await conn . query (
292- `SELECT Id, Email FROM User WHERE Email = '${ email } ' AND IsActive = true LIMIT 1`
300+ `SELECT Id, Email FROM User WHERE Email = '${ this . sanitizeSoqlValue ( email ) } ' AND IsActive = true LIMIT 1`
293301 ) ;
294302 if ( query . records . length > 0 ) {
295303 return ( query . records [ 0 ] as { Email : string ; Id : string } ) . Id ;
@@ -299,7 +307,7 @@ class SalesforceCRMService implements CRM {
299307 private getSalesforceUserFromUserId = async ( userId : string ) => {
300308 const conn = await this . conn ;
301309
302- return await conn . query ( `SELECT Id, Email, Name FROM User WHERE Id = '${ userId } ' AND IsActive = true` ) ;
310+ return await conn . query ( `SELECT Id, Email, Name FROM User WHERE Id = '${ this . sanitizeSoqlValue ( userId ) } ' AND IsActive = true` ) ;
303311 } ;
304312
305313 private getSalesforceEventBody = ( event : CalendarEvent ) : string => {
@@ -661,10 +669,10 @@ class SalesforceCRMService implements CRM {
661669 // For an account let's assume that the first email is the one we should be querying against
662670 const attendeeEmail = emailArray [ 0 ] ;
663671 log . info ( "[recordToSearch=ACCOUNT] Searching contact for email" , safeStringify ( { attendeeEmail } ) ) ;
664- soql = `SELECT Id, Email, OwnerId, AccountId, Account.OwnerId, Account.Owner.Email, Account.Website${ extraFields } FROM ${ SalesforceRecordEnum . CONTACT } WHERE Email = '${ attendeeEmail } ' AND AccountId != null` ;
672+ soql = `SELECT Id, Email, OwnerId, AccountId, Account.OwnerId, Account.Owner.Email, Account.Website${ extraFields } FROM ${ SalesforceRecordEnum . CONTACT } WHERE Email = '${ this . sanitizeSoqlValue ( attendeeEmail ) } ' AND AccountId != null` ;
665673 } else {
666674 // Handle Contact/Lead record types
667- soql = `SELECT Id, Email, OwnerId, Owner.Email${ extraFields } FROM ${ recordToSearch } WHERE Email IN ('${ emailArray . join (
675+ soql = `SELECT Id, Email, OwnerId, Owner.Email${ extraFields } FROM ${ recordToSearch } WHERE Email IN ('${ emailArray . map ( ( e ) => this . sanitizeSoqlValue ( e ) ) . join (
668676 "','"
669677 ) } ')`;
670678 }
@@ -898,7 +906,7 @@ class SalesforceCRMService implements CRM {
898906 if ( ! accountId && appOptions . createLeadIfAccountNull && ! contactCreated ) {
899907 // Check to see if the lead exists already
900908 const leadQuery = await conn . query (
901- `SELECT Id, Email FROM Lead WHERE Email = '${ attendee . email } ' LIMIT 1`
909+ `SELECT Id, Email FROM Lead WHERE Email = '${ this . sanitizeSoqlValue ( attendee . email ) } ' LIMIT 1`
902910 ) ;
903911 if ( leadQuery . records . length > 0 ) {
904912 const contact = leadQuery . records [ 0 ] as { Id : string ; Email : string } ;
@@ -973,17 +981,17 @@ class SalesforceCRMService implements CRM {
973981 }
974982
975983 for ( const event of salesforceEvents ) {
976- const salesforceEvent = ( await conn . query ( `SELECT WhoId FROM Event WHERE Id = '${ event . uid } '` ) ) as {
984+ const salesforceEvent = ( await conn . query ( `SELECT WhoId FROM Event WHERE Id = '${ this . sanitizeSoqlValue ( event . uid ) } '` ) ) as {
977985 records : { WhoId : string } [ ] ;
978986 } ;
979987
980988 let salesforceAttendeeEmail : string | undefined = undefined ;
981989 // Figure out if the attendee is a contact or lead
982990 const contactQuery = ( await conn . query (
983- `SELECT Email FROM Contact WHERE Id = '${ salesforceEvent . records [ 0 ] . WhoId } '`
991+ `SELECT Email FROM Contact WHERE Id = '${ this . sanitizeSoqlValue ( salesforceEvent . records [ 0 ] . WhoId ) } '`
984992 ) ) as { records : { Email : string } [ ] } ;
985993 const leadQuery = ( await conn . query (
986- `SELECT Email FROM Lead WHERE Id = '${ salesforceEvent . records [ 0 ] . WhoId } '`
994+ `SELECT Email FROM Lead WHERE Id = '${ this . sanitizeSoqlValue ( salesforceEvent . records [ 0 ] . WhoId ) } '`
987995 ) ) as { records : { Email : string } [ ] } ;
988996
989997 // Prioritize contacts over leads
@@ -1266,7 +1274,7 @@ class SalesforceCRMService implements CRM {
12661274
12671275 // Get the associated record that the event was created on
12681276 const recordQuery = ( await conn . query (
1269- `SELECT OwnerId, Owner.Name FROM ${ recordType } WHERE Id = '${ id } '`
1277+ `SELECT OwnerId, Owner.Name FROM ${ recordType } WHERE Id = '${ this . sanitizeSoqlValue ( id ) } '`
12701278 ) ) as { records : { OwnerId : string ; Owner : { Name : string } } [ ] } ;
12711279
12721280 if ( ! recordQuery || ! recordQuery . records . length ) {
@@ -1300,7 +1308,7 @@ class SalesforceCRMService implements CRM {
13001308 public getAllPossibleAccountWebsiteFromEmailDomain ( emailDomain : string ) {
13011309 const websites = getAllPossibleWebsiteValuesFromEmailDomain ( emailDomain ) ;
13021310 // Format for SOQL query
1303- return websites . map ( ( website ) => `'${ website } '` ) . join ( ", " ) ;
1311+ return websites . map ( ( website ) => `'${ this . sanitizeSoqlValue ( website ) } '` ) . join ( ", " ) ;
13041312 }
13051313
13061314 private async getAccountIdBasedOnEmailDomainOfContacts ( email : string ) {
@@ -1334,7 +1342,7 @@ class SalesforceCRMService implements CRM {
13341342
13351343 // Fallback to querying which account the majority of contacts are under
13361344 const response = await conn . query (
1337- `SELECT Id, Email, AccountId FROM Contact WHERE Email LIKE '%@${ emailDomain } ' AND AccountId != null`
1345+ `SELECT Id, Email, AccountId FROM Contact WHERE Email LIKE '%@${ this . sanitizeSoqlValue ( emailDomain ) } ' AND AccountId != null`
13381346 ) ;
13391347
13401348 SalesforceRoutingTraceService . searchingByContactEmailDomain ( {
@@ -1836,7 +1844,7 @@ class SalesforceCRMService implements CRM {
18361844 const existingFieldNames = existingFields . map ( ( field ) => field . name ) ;
18371845
18381846 const query = await conn . query (
1839- `SELECT Id, ${ existingFieldNames . join ( ", " ) } FROM ${ personRecordType } WHERE Id = '${ contactId } '`
1847+ `SELECT Id, ${ existingFieldNames . join ( ", " ) } FROM ${ personRecordType } WHERE Id = '${ this . sanitizeSoqlValue ( contactId ) } '`
18401848 ) ;
18411849
18421850 if ( ! query . records . length ) {
@@ -1863,7 +1871,7 @@ class SalesforceCRMService implements CRM {
18631871
18641872 // First see if the contact already exists and connect it to the account
18651873 const userQuery = await conn . query (
1866- `SELECT Id, Email FROM Contact WHERE Email = '${ attendee . email } ' LIMIT 1`
1874+ `SELECT Id, Email FROM Contact WHERE Email = '${ this . sanitizeSoqlValue ( attendee . email ) } ' LIMIT 1`
18671875 ) ;
18681876 if ( userQuery . records . length > 0 ) {
18691877 const contact = userQuery . records [ 0 ] as { Id : string ; Email : string } ;
@@ -1909,7 +1917,7 @@ class SalesforceCRMService implements CRM {
19091917 if ( ! accountId ) return ;
19101918
19111919 const accountQuery = ( await conn . query (
1912- `SELECT ${ lookupField . name } FROM ${ SalesforceRecordEnum . ACCOUNT } WHERE Id = '${ accountId } '`
1920+ `SELECT ${ lookupField . name } FROM ${ SalesforceRecordEnum . ACCOUNT } WHERE Id = '${ this . sanitizeSoqlValue ( accountId ) } '`
19131921 ) ) as {
19141922 // We do not know what fields are included in the account since it's unqiue to each instance
19151923 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2043,7 +2051,7 @@ class SalesforceCRMService implements CRM {
20432051 private async findContactByEmail ( email : string ) {
20442052 const conn = await this . conn ;
20452053 const contactsQuery = await conn . query (
2046- `SELECT Id, Email FROM ${ SalesforceRecordEnum . CONTACT } WHERE Email = '${ email } ' LIMIT 1`
2054+ `SELECT Id, Email FROM ${ SalesforceRecordEnum . CONTACT } WHERE Email = '${ this . sanitizeSoqlValue ( email ) } ' LIMIT 1`
20472055 ) ;
20482056
20492057 if ( contactsQuery . records . length > 0 ) {
@@ -2054,7 +2062,7 @@ class SalesforceCRMService implements CRM {
20542062 private async findLeadByEmail ( email : string ) {
20552063 const conn = await this . conn ;
20562064 const leadsQuery = await conn . query (
2057- `SELECT Id, Email FROM ${ SalesforceRecordEnum . LEAD } WHERE Email = '${ email } ' LIMIT 1`
2065+ `SELECT Id, Email FROM ${ SalesforceRecordEnum . LEAD } WHERE Email = '${ this . sanitizeSoqlValue ( email ) } ' LIMIT 1`
20582066 ) ;
20592067
20602068 if ( leadsQuery . records . length > 0 ) {
0 commit comments