February 25, 2022

Solving Shopify’s Content Security Policies Requirement

Did your app get rejected from the Shopify app store with this message?

1. App must set security headers to protect against clickjacking.

Your app must set the proper frame-ancestors content security policy directive to avoid click jacking attacks. The ‘content-security-policy’ header should set frame-ancestors https: //[shop], where [shop] is the shop domain the app is embedded on.


Here’s how we solved it. But first:

What are Content Security Policies and Frame Ancestors?

Content security policies (CSP) allow developers to prevent a number of potential vulnerabilities in the browser. In the case of Shopify and embedded apps — the primary concern is click hijacking.

You can solve this by setting a CSP header with the ‘frame ancestor’ value set to the URL of the store that is requesting the page. Doing this will ensure that only Shopify can embed your app on their website.

When Shopify embeds your app into the admin, they send along the shop URL as a parameter of the request. You need to grab this value and dynamically set the CSP header value so that the page can be embedded safely.

Our App Requirements

When we developed Bloom — our pickup and delivery date picker app — we found ourselves where a lot of other app developers find themselves when they submit their app for the first time. We were rejected for a few reasons — one of which was not having the correct content security policies set on our app headers.

Bloom is a CSR (client-side rendered) app, built with create react app. This made our hosting arrangement really simple — the app is uploaded to an S3 bucket in AWS and sits behind a CloudFront distribution. At the time of writing, AWS doesn’t let you set headers on S3 objects, so we had to go digging for an alternative solution.

If you’re not hosting your app in S3, you may still find the information useful for your own implementation. We’ve also included some additional links at the bottom of this article for other hosting setups.

Lambda@Edge to the Rescue

The solution for Shopify apps hosted in S3 is to create a Lambda@Edge function that gets assigned to the CloudFront distribution and modifies the headers for each request. Since the header we need to set is dynamic, we’ll need to pull that value from the request parameters and drop it into the header value.

Tip: Lambda@Edge functions must be created in the us-east-1 region!

The code for the lambda is quite short — a bit of boilerplate with only a line or two of code specific to the dynamic value we need to insert. We also add some other security headers while we’re at it.

In the function above, we pull the shop parameter off the request and insert it into the frame ancestor header on line 21. The shop parameter should exist on every request that is made from the Shopify admin. If your app is not embedded, you’ll want to set this value to ‘none’ rather than the dynamic shop value.

After saving your function, you should see a field on the right hand side of the Function Overview section called Function ARN. Copy and save this string — we’ll need it in the next step!

Breaking the News to CloudFront

Now we need to tell CloudFront to run this lambda function on every request made to our app (like middleware). Lucky for us, this is made fairly simple with CloudFront’s function associations.

We already had a CloudFront distribution setup for our app during development, but if you’re setting up for the first time, you can create the distribution and assign the function simultaneously.

We can find the function associations by opening the distribution and navigation to the Behaviors tab.

If you don’t have any behaviors setup, you can click the Create new button in the top right. If you have an existing behavior, click the little circle on the left (surely AWS can make this look more clickable?) and then click the Edit button on the top right.

Scroll all the way to the bottom and for the Origin Response association, select Lambda@Edge as the function type, then paste the Function ARN value that we saved from the last step into the text field.

That’s it — go ahead and hit save!

But I’m Not Using S3!

This process will obviously vary depending on how you’re hosting your app, but the core principle remains the same. If you’re still struggling to get approved after implementing this header, try:

  • Printing out the header you’re going to output to double-check it has the correct value. Remember the format needs to be: frame-ancestors
  • Checking that you’re outputting the dynamic shop URL based on the query string parameter (wildcards don’t work!).
  • Opening your embedded app in your test store and checking the headers are being returned in the request using Chrome Dev Tools (F12).
  • Googling around to learn how to set headers in your hosting environment.

We’d love to hear how you approached setting this header for your hosting environment. Leave a comment with your approach to help future readers!

Additional Links

If you find other helpful resources, let us know and we can include them here.

For a NodeJS server using express, this article might be useful:

Recent Articles