0

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?

  • Is the endpoint using TLS 1.2? I see you have an 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. – Daniel Ballinger Jun 12 '17 at 22:43
  • First we make a call to our endpoint, and we get back a JWT token. Then, on all successive calls to our endpoint, we use the Authorization header to pass the JWT to our endpoint. – LRK9 Jun 19 '17 at 16:04
3

Insecure endpoint sounds like you are using http instead of https. You can schedule time with someone on the AppExchange team to review your issues and get feedback.

| improve this answer | |
  • So when they refer to "endpoint" they always mean a URL that is on my side? They are not talking about an Ajax call that I make to another VisualForce page? – LRK9 Jun 19 '17 at 16:02
  • endpoint means a URL. Visualforce pages have URLs too, but my guess is they are referring to the value of SameDayConfigurationModel.baseEndpoint – Matt Kaufman Jun 19 '17 at 21:35
  • Okay, I guess I was using our dev endpoint. Our production endpoint is behind the AWS API Gateway, which enforces all the HTTPS encryption. – LRK9 Jun 20 '17 at 19:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.