Skip to Content
Technical Articles

Inbound HTTPS with CSRF Protection in CPI Integration Flows

Intro

The entire concept of protection against Cross-Site Request Forgery (CSRF) attacks is relatively commonly faced when being put in context of discussions of securing exposed HTTP resources. From technical standpoint, the flow prescribes a caller to firstly obtain a CSRF token from the resource provider by sending HEAD or GET request with the header X-CSRF-Token = Fetch and looking for a value of the header X-CSRF-Token contained in the response from the resource provider which is a value of the CSRF token, and then pass the obtained CSRF token value in the header X-CSRF-Token of subsequently issued modifying requests, such as those sent using HTTP methods POST, PUT, PATCH and DELETE. Usage of a CSRF token is normally not required when issuing non-modifying requests, such as those sent using HTTP methods GET, HEAD and OPTIONS.

CPI natively supports enablement of CSRF protection for inbound HTTPS connections in integration processes – this is one of out of the box standard features of the HTTPS adapter. Though, it is worth bearing in mind few notes and remarks that might become useful when coming across configuration and usage of this feature particularly in CPI.

 

Overview of baseline scenario

As a starting point and a baseline scenario, let’s introduce an iFlow that exposes a CSRF-protected HTTPS endpoint and produces a fixed response message when being called. Note that in HTTPS connection, a corresponding feature (CSRF Protection) has been activated:

In order to collect required details about processed requests (to be more precise, to be able to see content of messages that will arrive to the iFlow), I temporarily increase log level of the iFlow to Trace after the iFlow is deployed.

 

I’m going to use Postman to send requests to CPI and invoke this iFlow. Following general principles and requests flow that is required when consuming CSRF-protected HTTP resources, we are going to need two requests in Postman:

  1. Send a request to fetch a CSRF token. Note that the request to fetch a CSRF token is sent to the iFlow endpoint – in CPI, CSRF tokens are obtained from interface-specific endpoints of iFlows and not from a common interface-agnostic endpoint of the CPI tenant.
  2. Send a test message to the iFlow endpoint with the obtained CSRF token. I’m going to use a request with method POST to emulate a modifying request.

 

In real life scenarios, it is convenient to configure environment and use variables in Postman so that the CSRF token that is contained in a response provided for the first request, can be automatically retrieved by the script and inserted to a corresponding header of the second request using the variable, as well as to use variables to hold some environment and scenario specific information such as the iFlow endpoint and credentials used for authentication. For example, CSRF token can be read from a response for the first call and put to the variable in a one-line script in Postman:

pm.environment.set('csrf_token', pm.response.headers.get('X-CSRF-Token'));

followed by using the variable in the second call when populating the header X-CSRF-Token with the token value. Just a day before, Jerry Wang published a blog “Just a single click to test SAP OData Service which needs CSRF token validation” that contains detailed step by step explanation on how to use this technique to make usage of Postman more efficient.

Here however we need two simple requests and most of time we are going to use only one of them during tests, so I will deviate from these best practices and will not use variables in sake of more simplified and compacted illustration of requests sent by Postman, this will also help me to avoid usage of Postman Console to access information about effectively generated requests and actual values sent in variables’ place.

 

Iteration 1: Baseline scenario

Let’s put a baseline scenario under test.

At the beginning, we send a modifying request – POST request – without CSRF token in it to check that the iFlow will return authorization error:

 

We can also check that the iFlow will run successfully in case of sending a non-modifying request – let’s use GET request to illustrative that:

 

Next, we follow the flow mentioned above and firstly fetch a CSRF token and then send a POST request with the obtained CSRF token.

Send a request to fetch a CSRF token:

 

Send a test message with the obtained CSRF token:

 

Received responses for issued requests look as expected in Postman.

Now let’s have a look in CPI Message Monitor – interestingly, there are two messages there for the last test:

 

More detailed inspection of observed messages drives us to the following conclusion:

  • The first message corresponds to a HEAD request, which is the request to fetch a CSRF token:

  • The second message corresponds to a POST request, which is a test message.

Both above messages followed the same processing steps in the iFlow:

 

This is not what we would expect to see – the request to fetch a CSRF token shall only be handled by runtime to generate the CSRF token and send it back to a caller in case security checks are passed successfully, but shall not invoke the iFlow, as this would mean an unexpected message gets processed by the iFlow, which might potentially cause errors in the iFlow or in systems to which subsequent requests are sent if there are no relevant validations within the iFlow.

 

Iteration 2: Add special handling of request to fetch CSRF token

The most straightforward preventive measure that can be applied here, is to filter out CSRF token fetch requests right at the beginning of iFlow execution. One of technical options how this can be achieved, is to add a router step at the beginning of the integration process and introduce a dedicated “dead end” route for CSRF token fetch requests that doesn’t produce any response message, while other requests remain routed to a “main” route and lead to invocation of integration process steps.

The introduced route for capturing CSRF token fetch requests shall be defined with the relevant condition – the condition shall at least check the header X-CSRF-Token to have value Fetch, and preferably check an HTTP method that is used by the request. When fetching a CSRF token, some systems generate requests with an HTTP method HEAD (as the CSRF token is contained in the header and response body doesn’t bring value here, a caller might want to emphasize that and request callee not to produce body, but to only send headers), whereas some other systems generate requests with an HTTP method GET:

 

After this is done, a request to fetch a CSRF token is re-sent and messages generated in CPI are checked. We can still see that the message still got routed to the “main” route, which is not what we want to happen:

A closer look at this message suggests that not all conditions for the “dead end” route where met: the request was sent with an HTTP method HEAD, but the header X-CSRF-Token was missing:

 

Iteration 3: Allow header X-CSRF-Token

To ensure that the header in question is not removed by CPI runtime, that the message arrives to a router step with that header and the header value is evaluated, we need to explicitly allow the header X-CSRF-Token in runtime configuration of the iFlow:

 

After this is done, a request to fetch a CSRF token is re-sent once again and messages generated in CPI are checked. This time, we can see that the message got routed to the “dead end” route, and not to the “main” route:

 

Curiously, the displayed value of the header X-CSRF-Token looks cryptic, although the message met all conditions (including the one checking that the header X-CSRF-Token is passed with the value Fetch) – otherwise, it would have been routed to a default “main” route:

 

To make sense of it, let’s add a step before a router step to the iFlow and retrieve all HTTP headers of the received message. I will use the following Groovy script that retrieves HTTP headers and saves them as a message attachment:

 

import com.sap.gateway.ip.core.customdev.util.Message;

def Message processData(Message message) {
    
    StringBuilder builder = new StringBuilder();
    def headers = message.getHeaders();
    def messageLog = messageLogFactory.getMessageLog(message);
    
    headers.each { key, value -> builder << "${key}=${value}\n" };
    messageLog.addAttachmentAsString("Incoming message headers", builder.toString(), "text/plain");
    
    return message;

}

 

After re-sending a request to fetch a CSRF token, we can now see values of HTTP headers of the received message in plain text – note that the header X-CSRF-Token has value Fetch, as expected:

 

There is no mystery in the observed behaviour: the header X-CSRF-Token contains sensitive information, and its content is secured in Message Monitor by generating an SHA-256 hash from the original value and displaying hash value instead of the original value. This can be verified by generating SHA-256 hash for the value Fetch and ascertaining that the obtained value and the earlier observed value are identical:

 

Summary on final scenario and conclusion

Based on iterative analysis done above, here we go with a summary of adjustments that need to be introduced to the iFlow:

  • In runtime configuration of the iFlow, add header X-CSRF-Token to allowed headers,
  • In the integration process of the iFlow, add a router step and ensure that requests to fetch a CSRF token are routed to the dedicated route and do not get routed to the “main” process flow.

 

It shall be noted that this approach works well for requests that are classified as modifying requests – POST, PUT, PATCH and DELETE are most commonly used amongst them. The described approach will not work for non-modifying requests – GET, HEAD and OPTIONS – as it is not common to enable CSRF protection for resources that are accessed with these types of HTTP methods, and as a consequence, HTTPS adapter in CPI will not issue HTTP status code 403 for such requests sent with no CSRF token even if the iFlow configuration would imply enabled CSRF protection.

If the requirement is to ensure that the iFlow processes requests with only specific HTTP methods, it might be a good idea to add another route that will be used for requests with all HTTP methods except those explicitly specified in conditions of the “main” route, and if the request was sent with inappropriate HTTP method, a corresponding message with an HTTP status code 405 (Method Not Allowed) and empty body can be issued back to a caller:

 

Given that the in case of responses with HTTP status code 405, generally speaking, a server is not mandated to provide additional information in the response message, we can also make a bit of housekeeping in regards to headers that are contained in the produced response message and ensure that we clear all of them or at least those that we don’t want to communicate back to a caller. For details about background on this step, further reading is the blog “The Curious Case of the Unexpected Headers” written by Morten Wittrock.

 

In this blog, I deliberately don’t cover alternative ways of addressing requirements described above and limit suggestions with those based on capabilities of CPI. If the organization runs an API management solution (SAP API Management or its equivalents) and builds a layer of managed APIs on top of APIs available to the organization, another option could have been to introduce corresponding policies to verify an HTTP method of the incoming request on the managed API level, and ensure that only well-formed requests with allowed HTTP methods are sent to the endpoint of the iFlow running in CPI.

8 Comments
You must be Logged on to comment or reply to a post.
  • Great stuff again, Vadim. Definitely useful reference as we start to get more interesting integration scenarios in the cloudy world of CPI! 😉

    • Thank you, Eng Swee. It looks to me that the more we progress with various cloud integration scenarios and cloud services and applications, the more we have interesting findings and creative ways to address those requirements and also to deal with the entire integration design and development cycle.

  • Hi Vadim,

    Thanks for sharing 🙂 

    Can you please post one HTTPS configuration steps to establish connectivity between CPI and SAP as well.

     

    Thanks

    • I didn’t really use a SAP system as a caller in this integration and there was also no dedicated receiver there (in sake of simplification of the flow) – I emulated a caller system (which could be SAP or non-SAP) with Postman. If you meant HTTPS configuration for that (configuration of a connection from a sender to the integration process), then you can find corresponding screenshots at the beginning of the blog (in the section that describes a baseline scenario), where the relevant part that the blog draws attention to, is a configuration option to enable / disable CSRF protection.