Securing customer pages with a Shopify app proxy
Shopify’s Application Proxy feature allows apps to offer store owners and their customers more advanced functionality than what’s possible with a custom theme alone.
Quick recap if you’re rusty: a customer can visit a URL on a Shopify store
front (say, https://store.myshopify.com/a/orders
) and have that request
instantly passed along to your application. You’re then able to generate a
response (either plain HTML or Liquid) and pass that back to Shopify, who in
turn renders the page for the user.
A couple of common use cases for this sort of functionality are custom order tracking pages or customer wishlists.
Because these types of pages often contain customer information ranging from semi-sensitive (“John’s DVD Wishlist”) to quite sensitive (“What John Ordered”) to really sensitive (“John’s Address and Contact Information”), it’s really important to ensure that access to these pages is appropriately restricted.
In this post, I’m going to walk through the steps you should be taking to minimise the risks of exposing sensitive customer information. I’m also going to suggest a couple of things Shopify could do to make securing these types of pages a little bit easier.
Five concepts in proxy page security
1. Verifying proxy request signatures
This is essential for all applications using a proxy. In order to allow you to verify that a proxied request is coming from Shopify and not a malicious third party, Shopify signs all of its proxy requests using your application’s secret key. Your application should verify the supplied signature is correct before doing any further request processing.
See calculate a digital signature in the Shopify documentation for instructions on how to do this, including example code.
2. Identifying logged in customers
When Shopify passes along the proxy request to your application, there’s no way for you to tell whether a customer is logged in to their store account, as Shopify strips all cookie information in the proxied request, and doesn’t pass along that information in any other way.
Because the only piece of information your application receives is the URL itself, a request for customer-specific information needs to identify the customer in question within the path or query string itself. For example, a request for a specific customer’s wishlist might look something like this:
GET https://store.myshopify.com/a/wishlist?customer_id=1234
or like this:
GET https://store.myshopify.com/a/customers/1234/wishlist
Your application is able to parse the given customer ID and use that to fetch the appropriate data to return.
3. Wrapping customer information in a Liquid guard
If we stopped at the above step, we’ve left a massive security hole in our application - anyone at all could simply use a browser or a command line utility to execute the request
GET https://store.myshopify.com/a/wishlist?customer_id=1234
and fetch customer 1234’s wishlist.
To ensure that customer information is only ever displayed to a properly
logged-on user, you should wrap the entirety of your customer pages in a Liquid
guard (an {% if ... %}
statement) comparing the customer
ID in the URL with the ID of the currently logged on customer, like this:
<!--
The "1234" here would be dynamically rendered by your application based on
what was supplied in the URL or query parameters.
-->
{% assign supplied_customer_id = 1234 %}
{% if customer.id == supplied_customer_id %}
<h1>{{ customer.first_name }}'s Wishlist</h1>
... sensitive wishlist data here ...
{% else %}
<!--
If the customer IDs don't match, we can redirect them to the login page.
-->
<script>window.location.href = '/login/';</script>
{% endif %}
Unfortunately, there are three major limitations with this technique.
Firstly, we can only use it for authentication when we’re happy to require a user to both have a customer account and be logged on. There are a number of use cases where that’s not the case, such as order tracking pages – we’ll dive into that a little later on.
The second is that it only works if your application is returning a response
with the application/liquid
MIME type - it won’t work if you’re returning
pure HTML or a different type of content (for example, an image file).
Finally, and most crucially, this method undertakes authentication after our
application has received, processed and replied to the request. For this
reason, it’s only really suitable for preventing the display of Liquid-based
content to an unauthorised user, and can’t be used to check the authenticity
of a request that triggers some sort of action, such as a POST
request.
4. Simple authentication via URL parameters
To get around the issues identified above, we can pass some additional information in our URLs in order to verify the customer. This information should be something difficult to guess, but that both our Shopify store and our application has access to in order to verify that a request really is coming from a particular customer.
A simple, but slightly flawed, approach might say that a request for customer information needs to provide the customer’s email address in addition to their customer ID, like this:
GET https://store.myshopify.com/a/wishlist?customer_id=1234&email=customer@example.com
We can then generate links to sensitive information in our store’s Liquid templates and emails like this:
<a href="/a/wishlist?customer_id={{ customer.id }}&email={{ customer.email }}">My Wishlist</a>
Now, when a proxied request arrives at our application, we can check that the supplied email address matches up with the customer ID (our application, with access to the Shopify API, is able to maintain an up-to-date list of customers and their information).
Because we can use this authentication technique for requests from non-logged
in users, requests for images and non-Liquid-based resources, and POST
requests, it solves the issues identified with the Liquid guard technique
above.
5. Better authentication with URL hashes
Using the customer ID and email pairing for authentication is an improvement, but it’s still problematic. If a malicious user knows that a particular email address is in use on a store, cycling through a range of possible customer ID values for that user is a possible if slightly onerous task.
GET https://store.myshopify.com/a/wishlist?customer_id=1&email=customer@example.com
GET https://store.myshopify.com/a/wishlist?customer_id=2&email=customer@example.com
GET https://store.myshopify.com/a/wishlist?customer_id=3&email=customer@example.com
...
Given that our application pages could contain quite sensitive information, it would be good if we could come up with an authentication method that isn’t susceptible to this sort of attack.
The approach I usually take here is to use the customer’s email as a unique identifier and couple it with a specially-generated hash for authentication:
GET https://store.myshopify.com/a/wishlist?email=customer@example.com&hash=4e76434eea3c9d9cf9cb10bbf3f4a74b
The hash is obtained by generating the MD5 hash of a combination of the customer’s email and a secret key (pre-shared with both the store and the application), like so:
<a href="/a/wishlist?email={{ customer.email }}&hash={{ customer.email | append: settings.secret_key | md5 }}">My Wishlist</a>
You can see in the example above that we’re expecting the secret key to be
stored in the Shopify theme settings. An alternative would be to place the
secret key into a snippet called customer_hash.liquid
:
{{ customer.email | append: '3dc9d4ed99bb6fb68032ece899a28a7f' | md5 }}
and use it within your theme like so:
<a href="/a/wishlist?email={{ customer.email }}&hash={% include 'customer_hash' %}">My Wishlist</a>
On the application side, all incoming proxy requests can use the secret key to
generate the appropriate hash for the customer email passed in the URL and
check that it matches hash
parameter provided in the request.
Two important things to note about the secret key used to generate a customer hash:
- It shouldn’t be the same as the application secret generated by Shopify, as placing it in the theme settings or snippets will make it available to store owners;
- If your application is going to be installed to multiple stores, you should be creating a unique secret key for each store (again, because store owners have access to the secret key through their theme, you don’t want to be reusing them).
Remaining vulnerabilities
Bundling all of these strategies together isn’t always going to be required. If your application is used purely for the display of information to logged-in customers (a specialised order history app for example) and can be run entirely through proxied pages using Liquid template, then the Liquid guard method alone is enough to guarantee safety.
Once your apps require the manipulation of customer data or the fetching of images or non-Liquid resources, you’re going to need to introduction a URL based authentication strategy.
Using a combination of the above techniques – proxy signature verification, the addition of a Liquid guard, and hash-based URL authentication – is to my knowledge the best possible way to approach customer authentication for proxied pages.
However, there are still a couple of lingering concerns here. Placing all of the authentication information into the URL means that anyone with access to the URL gains access to the corresponding customer information. With the majority of Shopify sites unable to use SSL (a Plus-only feature), leaking URLs over an insecure HTTP connection is a very real possibility.
Implementing the hash-based approach also involves extra developer effort and can get tricky if you’re expecting your application to be installed on stores outside of your direct control – if a store owners wants to be able to link to a page served by your application, they’ll need to include the customer hash snippet when creating the URL.
Recommendations
To mitigate the issues above, I have a couple of suggestions for features Shopify could implement to improve security around proxied application pages.
1. Pass along the ID of any currently logged on customer
With every proxied request Shopify passes along to your application, it
adds a shop
query parameter to help your application identify the store the
request is coming from.
In addition to this, Shopify could pass along the ID of any customer that’s
currently logged in to the storefront, either along with the shop
parameter
in the query string or as a custom HTTP header (perhaps
X-Shopify-Customer-Id
).
Doing this would greatly simplify the authentication progress for all customer
pages where it’s required that a customer is logged in to their account. Pages
that require authentication without a customer login (such as order tracking
pages) would still need to use a URL-based method, but it would be possible to
reduce the risk of information leakage by doing something like still requiring
a customer account login after a certain amount of time has passed.
2. Offer SSL for all stores
With calls for HTTPS to be the default on all websites, let alone eCommerce sites, I think it’s worthwhile considering Shopify opening up SSL support for all stores on the platform. The historical argument against the feasibility of this (putting aside the business argument from Shopify’s perspective that SSL is a nice upsell feature for Shopify Plus) is that each domain served over SSL requires a separate IP address, which is an expensive proposition in a world where IPv4 addresses are pretty much exhausted.
Giving merchants the option of a $5/month upgrade to cover the costs would seem like a potential solution though, and greatly reduce the risk of accidentally leaking URLs over insecure HTTP connections.
Hopefully the information above has been useful, or at the very least provided a starting point for thinking about the security around your own applications’ proxy page security.
If you have any questions or suggestions, please drop me a line.