Insecurity of stagedUploadsCreate URL

I have been trying out using a bulk import operation using the customerSet mutation to import customers from an external system into Shopify.

Firstly you use stagedUploadsCreate to generate a URL, then upload the file to that URL before providing it to bulkOperationRunMutation to import.

The file you upload is publicly available. This is not acceptable for sensitive data like customer data.

There are two main security problems (at least):

  • URLs aren’t considered safe for storing secrets
  • Anyone with access to the files can read them

URLs aren’t considered safe for storing secrets

I understand the URL isn’t guessable. From my examples it takes the form something like:

https://shopify-staged-uploads.storage.googleapis.com/tmp/[id]/bulk/[UUID]/[file name].jsonl You can also make the file name a UUID or similar to make it even harder to guess.

However, URLs aren’t considered safe for storing secrets. After SSL termination (at Google in this case), the URLs are often logged so this would expose access to these files.

The files also expire, which makes a leak less likely, but anyone who accesses the file would of course be able to download it.

Anyone with access to the files can read them

The file isn’t encrypted, so anyone with access to the files bucket can read this file.

Have I missed a way to secure this data? All I have found so far is using the `customerSet` mutation individually for each customer, but that is obviously a lot slower than using the bulk import.

A similar question was asked here: Private uploading in GraphQL Bulk operation ? One of the answers suggests encrypting the file before uploading, but how would Shopify decrypt this data when importing?

Hi @Stephen_Upchurch

The URLs returned by stagedUploadsCreate aren’t bare public URLs. If you look at the response parameters, you’ll see they include:

  • x-goog-credential — a Google Cloud service account credential
  • x-goog-signature — a cryptographic signature
  • policy — an encoded policy document with conditions and an expiration
  • acl: "private" — the object’s access control is set to private

These parameters are required to upload to or access the file. Without the valid signature and credentials, the URL alone won’t grant access to the contents.

However if the security posture of staged uploads doesn’t meet your compliance requirements, the alternative you mentioned — using customerSet individually per customer — is the most direct option. You could also batch them using the GraphQL API’s standard request flow (not bulk operations) and manage your own throttling, which keeps data in the authenticated API channel end-to-end without any intermediate storage.

There’s no way to encrypt the JSONL file before upload and have Shopify decrypt it on the other end — Shopify doesn’t support client-side encryption for bulk operation inputs.

Hi @Liam-Shopify

Thanks for the response. Once a file has been created and I’ve uploaded data to it, I am able to copy and paste the file URL into a browser and access the file without any of those parameters.

E.g. all I need to do is a GET request to the URL which is something like:

https://shopify-staged-uploads.storage.googleapis.com/tmp/[id]/bulk/[some id]/[file name].jsonl

I’ve tried accessing the URLs on other machines/networks to make sure this is the case.