Securing paypal “smart buttons” via shared-secret hash signatures
Paypal promotes its “smart buttons” as a way to integrate purchasing on websites.1 However, by default, all of their purchasing logic, including purchase amounts for specific items, happens in the browser via javascript. This means that an attacker could edit javascript and modify payment amounts. That’s a problem if the vendor sets up webhook automation for paypal purchases to trigger business logic.
For example, the suggested code looks like this. Note that the amount is set on-page:
paypal.Buttons({
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
"amount": {
"currency_code": "USD",
"value": 40
}}]
});
},
// ... snipped ...
})
To secure smart button transaction business logic, most posts on the internet that I saw suggest that the only way to secure this is to have the paypal javascript call server routes which do the actual order creation and validation. The server saves validated order ids to a database, and any webhooks check that database before performing business logic.2
That approach works. However, it requires a
database. But the security-assignments.com storefront
is database-free, although it does use
some “serverless” cloud functions. My business logic adds email addresses to a restricted-access google group.
Customers pay (amount
) to have their email addresses (gcp_email
) added to that google group.
I am using two Google Cloud Functions: one for paypal order creation, and another for paypal webhook processing.
I thought of a way using symmetric-encryption digital
signatures to authenticate paypal orders. It uses a shared secret (shared_secret
) that both
google cloud functions have access to. The cloud function that creates the paypal order
saves the signature to the order metadata. The cloud function that handles the paypal webhook
checks that signature against its own calculation of the same.
My signature algorithm hashes three fields: (1) gcp_email
, (2) amount
, and (3) shared_secret
.
Like this in python (approximate code location):
import hashlib
import os
m = hashlib.sha256()
SHARED_SECRET = os.environ['SHARED_SECRET']
hash_me = f'{gcp_email}|{amount}|{SHARED_SECRET}'
m.update(hash_me.encode())
signature = m.hexdigest()
# add the signature to the paypal json
order_json = {
"purchase_units": [{
"custom_id": signature,
"invoice_id": gcp_email,
"amount": {
"value": amount,
"currency_code": "USD"
},
# ... snipped ...
}]
# ... snipped ...
}
Then later, the paypal webhook checks the signature, before adding gcp_email
to the google group (approximate code location):
webhook_event_json = request.get_json()
custom_sig = webhook_event_json['resource']['custom_id']
def custom_sig_verify(gcp_email, amount, custom_sig):
SHARED_SECRET = os.environ['PAYPAL_SHARED_SECRET']
hash_me = f'{gcp_email}|{amount}|{SHARED_SECRET}'
m = hashlib.sha256()
m.update(hash_me.encode())
sig = m.hexdigest()
return sig == custom_sig
The paypal javascript on https://security-assignments.com/store calls the first webhook to create the order, and doesn’t do any server-side processing for the approval step. Similar to this:
paypal.Buttons({
createOrder: function(data, actions) {
return fetch(gcp_cloud_function_paypal_order_create_endpoint, {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({'gcp_email': gcp_email, 'paypal_mode': PAYPAL_MODE})
}).then(function(res){
return res.json()
}).then(function(data){
return data.id
})
},
onApprove: function(data, actions) {
return actions.order.capture().then(function(orderData) {
// Full available details
console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
// ...snipped...
});
},
})
Tada! No database!
Since my paypal client-id is still in the sourcecode, an attacker could still craft api calls and be annoying and pay me whatever they wanted, and I would have the inconvenience of having to refund their order. But the business logic would never trigger.
Go on and F12 https://security-assignments.com/store!
Links to the source code repos below. If you see a problem or an improvement, please open an issue on the repos!
Go to paypal order creation repo
-
If you don’t need any fancy modification, I suggest using their “buy now” hosted buttons. Since the button logic is hosted on paypal.com servers, the buttons are secure against client-side modification. You won’t find that page linked from hardly anywhere on paypal.com, though! ↩
-
The other button security solution I saw on paypal requires Java and is legacy. ↩
Dave Eargle is a Senior Consultant in Cybersecurity Assessment at Carve Systems. More about the author →
This page is open source. Please help improve it.
Edit