Data leakage exposed millions of customer records

Published by Tobias Hofmann on

12 min read

In Germany you must make sure that your car is equipped for the weather. For winter, this means that you mount winter tires. While you can use those also in the summer, it is better to switch back to summer tyres. In real life this means that car owners are changing their tyres in April and October. While it is hardly snowing in my region, you never know and when traveling to the black forest these are mandatory. Therefore, this spring I was trying to find a company for changing my tyres (yes, I can do this in my own, but I am also lazy. In retrospect, it would have saved me massive time changing them on my own). Searching the internet, I came across the web site of a very large and well-known tyre company that operates their own branch offices around Germany. Their head quarter is located close to where I live (Lokalpatriotismus!). The web site offered an online form to find an office and to schedule an appointment. Looked like a win-win situation.

So I started my journey and as my dear readers might expect, it will be exciting. My adventure – and yours now too! – started as I clicked on the appointment link. The app opened and saw this (sorry, texts are in German).

This already looked a little bit familiar: overall layout, tiles, header, header text not in the middle thanks to the logo. Looking at the URL it was clear: SAP technology involved.

URL: https://company—co–kg

Yes, I found an app deployed on SAP Cloud / BTP. In the wild! In usage! Live! Intended to be used by normal people that have no idea what SAP is. A live app to drive business. Yes, custom domains are very expensive. Saving money here means that a potential attacker already knows what kind of app is deployed here and its location. Yes, cloud of course, but cloud as in: a server in data center eu10 from SAP. Not sure why SAP wants to benefit here and asks for money to offer the lowest level of protection. Personally, I think this exposes too much information that no one should know or needs to know. At least should not be made so obvious. As long as SAP is charging (too much) money for a custom domain name, customers need to find a justification to pay for it. Maybe this post helps.

The app follows an interesting concept: you can use it either as an anonymous user up to the part where you confirm your appointment, or as a logged-on user from start. This means that the app needs to provide information (car type, service, locations, etc.) for both anonymous and logged in users. This information is read in both use cases from the backend. Maybe you can already foresee the problem I encountered: only for scheduling the appointment a logged-in user is necessary. To log on a new user can be requested or an existing one used. The link to the logon server reveals that the SAP IdP service for SAP BTP is used.

Interesting was that the app is developed in UI5. That’s a technology I understand a little bit. It also means that I am able to roughly get a grasp understanding of the architecture, which services are called and what routes are available. Most of this information I can obtain by reading the file manifest.json. The network trace also showed that calls to a file metadata.xml were done, so an OData backend is consumed. The URL contained sap/opu/odata/sap, indicating that an SAP Gateway system was called. Either logged on or not, the app called two OData service.

Service 1: NGM_TECH/sap/opu/odata/sap/ZTB_AB_CUST_SERVICE_SRV/$metadata

Service 2: NGM/sap/opu/odata/sap/ZTB_AB_CUST_SERVICE_SRV/$metadata

The service name was the same: ZTB_AB_CUST_SERVICE_SRV. The path was different: NGM vs. NGM_TECH. This is how this looked in the file manifest.json:

For both services the metadata and document info were the same. Ex quo sequitur: identical services. To find out why there were to identical OData service I took a look at the source code of the UI5 app. Adding the debug parameter allowed me to read the source code of the UI5 app: index.html?sap-ui-debug=true

Looking closer at the source code I found this code:

determineServiceSource: async function () {
  let handleAuth = () => {this.setODataModel("orderbook_private");};
  let handleUnauth = () => {this.setODataModel("orderbook");};
  this.checkAuthentication(handleAuth, handleUnauth);

The code checked if a user is authenticated or not and sets the OData model to be used by the app accordingly. That’s why there are two models in the manifest.json and on the SAP backend. While offering the same service, one is for anonymous users, and one for authenticated users. Remember: this is because the app wants to make it easy for the end user to book an appointment. Instead of already blocking a (potential) customer by asking to log on beforehand, the app allows the person to select services and search for an appointment date. Only when it is needed to finalize the booking, the app asks the user to authenticate.

The metadata shows some interesting entities offered by the services. For instance, I found an entity set CustomerSet. As a logged-on user, it was possible to call the NGM service and query the CustomerSet for my own user. I called the set and instead of returning all customers it only returned my customer entry. Actually, the service returned all the customers I had the permission to access, and of course, I can only access my own customer record. An alternative approach for this could be to use an OData function to get the user information of the current user.

Calling NGM/sap/opu/odata/sap/ZTB_AB_CUST_SERVICE_SRV/CustomerSet returned my customer record.

After going through the self-registration process the SAP system already knew some of my information (name, e-mail, location), yet other information I did not inform during registration were set to dummy values. Particularly the postal/zip code was set to value 9999.

So far what I had found on the internet was an UI5 app and an OData service linked to an SAP Gateway system. The metadata showed interesting information like CustomerSet. Access to the service was possible to every person by simply registering with a valid email. Of course, I tried out to filter the entity set and see if I can access other records besides just mine. It did not work. The current user information was used internally by the ABAP coding of the OData service to be used as a filter. Trying to query e.g., for the zip code 76646 returned nothing as my user had 99999 assigned.


So far, so good. But I remembered that the same service existed two times. And the NGM_TECH seemed to me like a technical user is used to access the SAP backend. After all, how to access an SAP Gateway OData service when you are not logged in? Maybe the technical user had the permission to query for other zip codes or some authorization check is deactivated? Maybe some IF on sy-uname is missing? I already had the filter parameter and all it took me to try this out was to add _TECH to the URL. Well, that’s what I did. Writing a whole post about an OData service that is 100% secured won’t make much sense, right?

I called the service for anonymous users as anonymous user: NGM_TECH/sap/opu/odata/sap/ZTB_AB_CUST_SERVICE_SRV/CustomerSet?$filter=substringof(%2776646%27,PostalCode)

I did not expect it to work. But it did. The service returned the customer entities for the users linked with the zip code 76646. I was surprised. I tried an anonymous browser window: it worked. I tried postman: it worked. I switched the service to the protected one, it did not work. Switched back to the anonymous service and it worked. I cleared so many browser caches and cookies, yet: it worked.

The OData service returned the available customer data: firstname, lastname, address, phone, mobile, salutation, account number. The complete customer records. I tried it with different zip codes, and it always worked. Reminder: it worked using the anonymous OData service! That was now a whole new level of SNAFU. Looking at some data it was clear: yes, the data is valid. It is real customer data. Looking at their branch offices I used the zip code of some more crowded places and the query failed due to a timeout. Thanks to slow performing SAP Gateway system it was not possible to get the customer data when there were too many customers inside a given zip code. Making it not impossible to get the customer data, but harder.

Getting access to customer data is bad. Particularly that the data was available to anonymous users. Basically, every single person with internet access had access to their customer data. That’s really bad. But how much data leaked? On their homepage an overview of offices is available. The company operates in several cities. Their business is not only with individual persons like me, but also companies. The actual number of customer records that unintentionally was exposed through the OData query was high. My guess is that it was at least a few million records.


As soon as I found out that customer data is leaking, I reported this to the data protection officer of the company. This was a really nice experience. I reported my findings on 12. April, late evening. Two days later, 14. April, noon, I got an answer to my email. The problem was acknowledged and closed already on day 13. April, in the morning. Trying to query the service as anonymous user returns now an error. Caused by missing authorization.

Luckily the service was quite new, and the error that allowed anonymous users to obtain customer records was also only introduced recently. They ran an analysis, and it seems that only I found the leak. From reporting to closing the leak it took them roughly 12 hours. I am also impressed that once more this issue showed that the SAP world is a small one. Turns out I know the people that developed the app and the company is well known among members of the Stammtisch. Stammtisch Baden, connecting the cool people of the SAP world.


A small error in a service can lead to a massive data leakage. I guess that the technical users for the anonymous service had only the wrong authorization assigned, or an IF in the coding was wrong. Nevertheless, the problem was fixed rapidly, also implying that the root cause was not too complicated nor introduced by design. A small inattention in the coding can cause big problems. Be aware of this and better triple check coding that might give access to sensitive data. Try to create OData requests as test cases and run them against your service. I think it would be great if SAP can provide an OData query tool that automatically creates queries for a given OData service.

After the security hole was fixed, I tried again to schedule an appointment through the app. In the end I did not book the service. While everything is nice with the app, service selection, online scheduling, the most important part for me to know before booking a service is to know how much it will cost. And exactly this information is not shown. Why should I schedule a service without knowing how much I’ll have to pay? I tried to find this information somewhere on their homepage without luck. I tried to call their service help desk, but after 10 minutes waiting and no one picking up the phone I gave up. I changed my tyres at another company. Lesson learned here: an app supports a business process. If the process is broken, first fix the process.

Let the world know
Categories: Technology

Tobias Hofmann

Doing stuff with SAP since 1998. Open, web, UX, cloud. I am not a Basis guy, but very knowledgeable about Basis stuff, as it's the foundation of everything I do (DevOps). Performance is king, and unit tests is something I actually do. Developing HTML5 apps when HTML5 wasn't around. HCP/SCP user since 2012, NetWeaver since 2002, ABAP since 1998.


Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.