/~r/sysadmin/ Have you tried turning it on and off again?

Google App Engine Blobstore Security

This post is about the security of the Blobstore Python API for Google App Engine. Specifically, it’s about the store-and-forward-request model that GAE suggests, where a visitor can upload files directly to Blobstore. Some parts of it also apply to Blobstore in other GAE languages or file upload systems in general.

When designing GAE apps using Blobstore, keep these exploits in mind:

Content-Type tricks

Blobstore will save the Content-Type header that the user specifies. You should be careful when serving uploaded files back to the user. If you’re using the X-AppEngine-BlobKey response header to serve an uploaded file to a user, then GAE will intercept the response and start serving the uploaded file. By default, this response will contain the Content-Type specified when the user originally uploaded the file. An attacker can use an text/html content type to achieve XSS.

You can mitigate this attack by always specifying your own Content-Type alongside your X-AppEngine-BlobKey header. Your content type will override the user’s.

You should also set the nosniff header and force the UA to treat the file as an attachment. The combination of the following 3 headers is most effective in preventing user agents from interpreting uploaded files as code:

resp.headers["Content-Type"] = "video/mp4"
resp.headers["X-Content-Type-Options"] = "nosniff"
resp.headers["Content-Disposition"] = "attachment; filename=foo.mp4"

Phony Blobstore callbacks

If you’re using the google.appengine.ext.blobstore.create_upload_url API to generate upload URLs, then your success_path isn’t exposed to the user. (It’s kept in the Datastore instead.) However, if your success_path is an easily-guessable URL, it’s likely that your app is vulnerable to phony Blobstore callbacks. Instead of hitting your success_path via Blobstore’s store-and-forward-request mechanism, an attacker could send a request to your success_path directly.

What are the implications? Well, if your Blobstore upload callback does something like “save the blob key as a new file under the current user’s account”, then an attacker could take over ANY Blobstore file if they know the blob key.

You can imagine a payload like:

POST /api/clips/aghkZXZ-d0b36P37UvqHanme+JjN0ikeLG5GCO7tTelSamPEYtJmKRcfO/ HTTP/1.1
Content-Type: multipart/form-data; boundary=9a1fc2ae10abda1b0456cc19d222
Content-Length: 312

--9a1fc2ae10abda1b0456cc19d222
Content-Type: message/external-body; blob-key="encoded_gs_file:3g8W6uTy6B4GekBYOnPK5zu2eSkr9A6s8xc3IWZtzn6xXTanlY46cB0qGvDR6+hmgPiwGcw="; access-type="X-AppEngine-BlobKey"
Content-Disposition: form-data; name="file_payload"; filename="foo.mp4"

--9a1fc2ae10abda1b0456cc19d222--

As far as I can tell, even Google’s own python Blobstore upload handler doesn’t protect against phony requests like this. You can take some solace in the fact that Blobstore blob keys don’t appear to be obviously guessable, but there are a few issues with this thinking:

  1. The GAE docs encourage using key.urlsafe() for passing around base64-encoded keys, so it’s not unreasonable to assume some GAE apps expose Blobstore blob keys, by design.
  2. Blob keys aren’t guaranteed to be unguessable anyway. If one day, GAE decides to start using sequentially-assigned blob keys, then this attack would suddenly become much more feasible.

Suggestions

I’m planning on moving my application to Google Cloud Storage Signed URLs. GAE already offers a Python client for this, and it comes with authentication magic built-in! So you don’t need to set up service accounts to allow GAE apps to access GCS. Blobstore for GAE is probably soon-to-be deprecated anyway.