Leo Express is a Czech company operating train and bus lines in Central Europe. They provide an option of registering an account and joining loyalty programs as well as getting points for each ride.
When I signed up, I noticed that on every page load a GraphQL request is sent to the server, which returns my account information in JSON.
GraphQL is a query language for APIs (a popular alternative to REST) that returns data defined on the client side in a single request.
Here’s how the content of this POST request looked like:
{
"query":"
query getActualUserDataQuery($email: String, $token: String, $timestamp: Int, $locale: String) {
getActualUserData(email: $email, token: $token, timestamp: $timestamp, locale: $locale) {
token
user {
id
login
firstName
lastName
phone
address_state
address_city
address_street
address_zip
facebook_id
google_id
sex
currency
language
profile_picture
clubmember
credit_bonus
credit_standard
smilebus
distance
agreements {
type
enabled
__typename
}
__typename
}
error {
code
message
__typename
}
__typename
}
}
",
"variables":{
"email":"info@example.com",
"token":null,
"timestamp":0,
"locale":"cs"
},
"operationName":"getActualUserDataQuery"
}
In the variables object we will be interested in the email and token fields, where my email and an authorization token were filled in. I didn’t expect this request to still work even when the token was changed or wasn’t there at all.
I also tried removing cookies from the request headers in case it was being authorized thanks to them. But this wasn’t the case.
This meant it will return the data for any registered email that was entered.
The response body contained information like name, phone number, full address and other things,
e.g. a connected facebook/google account.
Part two, XSS and credit cards
Another problem in connection to a reflected XSS allowed us to get information about saved credit cards of a logged-in user.
Upon order completion, a redirect will be made to the following URL with a message that the tickets were sent to the user’s email address.
https://www.leoexpress.com/en/order-confirmation?order=12345&email=info@example.com&state=success
The issue was that the displayed email address was taken from the URL’s email parameter and special characters weren’t escaped before it was inserted to the page. Since the website didn’t have a Content Security Policy, it meant it was possible to execute arbitrary JavaScript code on the page.
Once any logged-in user clicks or is redirected to that URL, we gain practically unlimited access over their account.
If the user has a saved credit card in their profile, we have an access to the information about it. That includes the card type, date when it was added, and most importantly the first 6 and last 4 digits of the credit card number. That’s 10 digits from the total of 16. That might already be useful for something…
These vulnerabilities were fixed within three months of the initial report, which isn’t ideal, but better than nothing.