I’m fairly new to web development, and have a few questions about security with Shopify’s app proxy. For reference, I’m using Shopify’s QR code app example.
I currently have an app proxy set up to return a liquid page populated with information stored in my database. I’m currently verifying the request to the proxy’s endpoint on my server using Shopify’s recommended method (in Javascript instead of Ruby, if that matters), with the endpoint on my server being at /proxy_route/
. If the signatures don’t match I return a 401 error, and here’s where my first question comes in:
1. If the signature matches and the request is verified, is it safe to directly access the database instance to populate the page?
For example, I have something similar to the example’s QR code GET endpoint, but instead of calling this endpoint from my proxy I directly access the database, like so:
app.get("/proxy_route/", async (req, res) => {
if (verifyRequest(req, res) {
try {
const rawCodeData = await QRCodesDB.list(await "<shop_name>.myshopify.com"); // (changed to match with the GitHub page, in my example no sessions exist because it's a custom app intended for 1 shop)
const response = await formatQrCodeResponse(req, res, rawCodeData);
res.status(200).set("Content-type", "application/liquid").send(JSON.stringify(response)); // again, just for simplicity. In my code this data gets processed and inserted into a real liquid page and that get's returned instead of raw results
} catch (error) {
console.error(error);
res.status(500).send(error.message);
}
else {
res.status(401).send("Not authorized").
}
});
From what I understand, this should be safe as any unauthorized request gets a 401 due to verifyRequest()
, correct?
For my next question, I’m serving another page at /proxy_route/post
that returns a static liquid page after verification is successful (exactly the same as above, minus any database stuff), but this page contains a form and image upload that I want to be able to POST into my database. I verify the user is signed in using liquid guards, like so:
{% if customer %} <show form> {% else %} <tell customer to sign in> {% endif %}
So I can guarantee the customer is signed in if they’re able to hit the "Submit" button. Here’s where my main question is, as I’m not sure how authentication should work for the POST request. The form will be for uploading information that’s tied to the customer’s account, so is it possible to get the current customer’s session and pass it along in the request (and how would I verify that)? Can I just pass {{ customer.id }}
and {{ customer.email }}
in the header and assume it’s valid, as I’ve already verified the original proxy request was valid? If I do that, what would stop anyone else from making a POST request with a random number instead of a real {{ customer.id }}
? I’m assuming the request will be coming from client-side Javascript (using <script>
tags), so that would mean there’s no Shopify-related authentication, correct? I’m also looking to make a way for customers to delete the content they posted to the database, so how would I verify that it’s the real customer deleting their own information? I’m kind of at a roadblock with this, so any insight would be much appreciated!
Update for clarity:
So I’m currently serving a simple form on my proxy page (for example, at /a/form/
. Shopify makes the request to my server containing the authentication signature, signed in customer id, etc, and I return the liquid file the customer can see, which contains the form to fill out:
<form action="/a/form/post" method="post"> (form contents) </form>
I have my server set up to accept a POST request to that endpoint (at <ngrok_url>/proxy_route/post
, but when I try doing it through the app proxy (exactly like the above code) I get a 421 error. If I change the POST url to go straight to the ngrok url, it accepts the post but none of the authorization/user info is present. My question is, how would I authenticate a request coming from the app proxy page? Do I just pass along whatever data I have access to (user’s email/account #/etc) and hope that’s secure enough? Because if I do that, in theory anyone from anywhere can start POSTing info under other people’s account, correct?
2
Answers
You do not need to validate the customer ID anymore as Shopify conveniently includes a valid customer Id in the secure header data, meaning people cannot fudge it. So in the theme, it is a check for the customer, and if they are logged in, you are assured the ID gets to Shopify Proxy as legit data.
I think the problem the author mentions is the following:
Even though an Shopify App Proxy adds hmac and certain other data to the request (customer_id IF logged in) and this then can be used to prevent bad actors to call your endpoint of your app directly, it doesnt prevent bad actors to call your App Proxy URL and there is no security in place to prevent the return data to be delivered.
That means if for example you create an app endpoint that takes an email and returns a name and birthdate, the app proxy secures nothing since anyone can bruteforce send requests with email to the app proxy URL (NOT your app endpoint, since that can be verified with the hmac added from teh App Proxy, but so what, anyone can call your App proxy instead).
Which basically means, App Proxies are not secure at all and should under no circumstances be used for sensitive data if the user is not logged in.
If the user is logged in, you can actually create your own hmac using your secret key and the logged in email at page load and check that in your app if the hmacs match. Like that you at least avoid that your app proxy can be brute-force called with millions of email addresses in case someone extracts your app proxy call (e.g. through the Network tab in the browser).