I'm trying to get a managed package into the Salesforce App Exchange. My app was rejected by the security review. One of the complaints was:
- Insecure Endpoint
My company sells data about private companies. Our customers are mostly salespeople who are looking for new leads. Our app allows them to run searches against our API. That is, when using our app, our customer is looking at a VisualForce page, and then they type in some words, hit the Submit button, and then our Apex controller does a web callout to our API. Our API lives on AWS and is served via the AWS API Gateway. When the data comes back, we don't save it to the database, we merely show a summary of each entry on a VisualForce page, but next to each entry is an "Import?" button, which the user can click to import the full profile. If they click "Import?" I don't want them to leave the page, so I tried to use Ajax so the import happens on a page that the user never sees.
This is the initial API callout, after the user has typed some criteria for the search, and then hit "Submit!"
public HttpResponse fetch(PrivateIncInfo__Search__c currentSearch) {
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
HttpResponse response;
// 2017-02-22 -- the API can be slow, so we set the timeout for 60 seconds. I was getting timeout errors.
request.setTimeout(60000);
ps = SameDaySessionModel.findCurrentSession();
if (ps == null) {
SameDaySessionModel.tellUserToLogin();
return response;
} else {
String endpoint = SameDayConfigurationModel.baseEndpoint + 'verbose/';
String company_name = currentSearch.Name;
String employee_count_lower_limit = currentSearch.PrivateIncInfo__employee_count_lower_limit__c;
String employee_count_upper_limit = currentSearch.PrivateIncInfo__employee_count_upper_limit__c;
String industry = currentSearch.PrivateIncInfo__industry__c;
String region = currentSearch.PrivateIncInfo__region__c;
String country = currentSearch.PrivateIncInfo__country__c;
String revenue_lower_limit = currentSearch.PrivateIncInfo__revenue_lower_limit__c;
String revenue_upper_limit = currentSearch.PrivateIncInfo__revenue_upper_limit__c;
String payload = '';
payLoad = '{ "company_name" : "' + company_name + '", "revenue_upper_limit" : "' + revenue_upper_limit + '", "revenue_lower_limit" : "' + revenue_lower_limit + '", "employee_count_lower_limit" : "' + employee_count_lower_limit + '", "employee_count_upper_limit" : "' + employee_count_upper_limit + '", "industry" : "' + industry + '" , "region" : "' + region + '" , "country" : "' + country + '" , "api_pagination" : "' + api_pagination + '" }';
request.setBody(payLoad);
request.setEndPoint(endpoint);
request.setMethod('POST');
request.setHeader('Accept', 'application/json');
request.setHeader('Content-Type', 'application/json');
request.setHeader('x-api-key', SameDayConfigurationModel.xApiKey);
request.setHeader('Authorization', ps.APIAuthorizationKey__c);
response = httpProtocol.send(request);
if (response.getStatusCode() == 403) {
System.debug('Status code was 403, which means we need to renew our login with the SameDay API.');
throw new SameDayAccessException('Status code was 403, which means we need to renew our login with the SameDay API.');
}
return response;
}
}
I might get back 300 results. We then show a short summary about each result, and we paginate this at 25 results per page. So if we got back 193 results we would paginate over 8 pages.
Next to each result is a button that asks "Import?"
If they click the "Import?" button, we do an Ajax call to another VisualForce page, which then does another API call, gets the data requested, and saves it to the Salesforce database.
The "Import?" link has this Javascript attached to it:
j$ = jQuery.noConflict();
function callImport(profileType, profileId) {
var importUrl = "{!baseUrl}/apex/SameDayImportEndpoint?profileType=" + profileType + "&profileId=" + profileId;
var jqxhr = j$.get( importUrl, function() {
}).done(function() {
alert( "Profile was imported" );
})
.fail(function() {
alert( "We are unable to import" );
});
}
On that other page, in the controller, we spark a long and complicated bit of import magic:
public void ImportProfile() {
String profileId = ApexPages.currentPage().getParameters().get('profileId');
String profileType = ApexPages.currentPage().getParameters().get('profileType');
System.debug('In SameDayImportController.ImportProfile() the profileType is ' + profileType + ' and the profileId is ' + profileId);
if (profileType == '') {
profileType = 'company';
}
profileType = profileType.toLowerCase();
System.debug('profileType is now lowercase: ' + profileType);
if (profileId == null) {
String message = 'Error: Expected profileId in the URL query parameters. It was null, therefore importProfile did nothing.';
System.debug(message);
SameDayUserMessageModel.addUserMessage(message, 'error');
// code stolen from here:
// http://salesforce.stackexchange.com/questions/54010/apexrest-controlling-http-error-codes-on-exceptions
RestResponse res = RestContext.response;
if (res == null) {
res = new RestResponse();
RestContext.response = res;
}
res.responseBody = Blob.valueOf(message);
res.statusCode = 400;
} else if (profileId == '') {
String message = 'Error: Expected profileId in the URL query parameters. It was an empty string, therefore importProfile did nothing.';
System.debug(message);
SameDayUserMessageModel.addUserMessage(message, 'error');
} else {
String endpoint = fetchUrlForProfile(profileId, profileType);
String SameDayJsonString = fetchProfile(profileId, endpoint);
List<Map<String, Object>> possibleSameDayData = SameDayApiModel.deserializeJson(SameDayJsonString);
Map<String, Object> profile = possibleSameDayData[0];
if (!profile.containsKey('bad_api_call')) {
if (!isDuplicateAccount(profile)) {
Account profileAccount = profileToAccount(profile);
// we need to save this right here, because we need the ID,
// to link all other items to this account
try {
if (Schema.sObjectType.Account.isCreateable()) {
insert profileAccount;
} else {
SameDayUserMessageModel.addUserMessage('Error! We were not allowed to create the Account Profile. Please talk to the Salesforce admin or manager at your organization, and ask for the appropriate permissions.', 'error');
}
peopleToContacts(profile, profileAccount);
websitesToNotes(profile, profileAccount);
investmentsToNotes(profile, profileAccount);
subsidiariesToNotes(profile, profileAccount);
List<PrivateIncInfo__SameDaySummaryForProfileImport__c> listOfRelationships = SameDayImportModel.importRelationships(profile, profileAccount, endpoint);
saveListOfSameDayImports(listOfRelationships);
List<PrivateIncInfo__SameDaySummaryForProfileImport__c> listOfInvestors = SameDayImportModel.importInvestors(profile, profileAccount, endpoint);
saveListOfSameDayImports(listOfInvestors);
SameDayUserMessageModel.addUserMessage('Imported: ' + profileAccount.Name, 'success');
} catch(System.DMLException e) {
System.debug('DMLException: Unable to save the main profile account from SameDay. This keeps us from having an Account.id to give to all other records generated for this profile.');
System.debug(e);
SameDayUserMessageModel.addUserMessage('FAILURE! Error importing: ' + profileAccount.Name + '. Error message: ' + e, 'error');
}
}
}
}
}
Does it seem likely that this is what Salesforce is complaining about when it says I have an "Insecure Endpoint"?
If yes, how should I change the architecture?
Authorization
header. Is that unique per custom. How is the value generated to pass to that header? Usually the security review would provide additional details about the security problem.