Netlify & Your Secrets

Apr 2 2020

Continuous deployment is awesome. Static websites are also awesome. What do you get when you combine the two? Probably something like Netlify.

However, there’s a bit of a problem if you’re using their serverless function features to abstract your privileged APIs.

What’s Netlify?

Netlify offers hosting and serverless features for static websites.

Those serverless features were designed to supercharge your static website. For example, you could delegate authentication to Identity, or form data collection to Forms.

Another neat feature is Functions, which brings the power of AWS Lambda to your site but with easier deployment and versioning.

A common usecase for Functions is to use it as a proxy for your privileged APIs that require some secret key. You’d set your secret keys as environment variables on Netlify’s end and they will be available at function runtime.

What’s the exploit?

The exploit involves an interaction between the two components: deploy previews and your environment variables.

Deploy Previews

Netlify offers the ability to generate deploy previews, which are status checks for PRs that build a live-preview of the changes. They’re incredibly useful since you can review a PR’s changes without having to pull, build, and verify your changes yourself as Netlify will do that for you by generating a preview at deploy-preview-<PR#>--<sitename>.netlify.com.

This feature is on by default.

Environment variables

Environment variables, as the name suggests, are variables that are accessible during your build. They can either be set through your team settings, your site settings, or the netlify.toml file. Your netlify.toml configuration will override your site settings, which will override your team settings. In other words, it will read the netlify.toml file first, then your site settings, then your team settings to determine the environment variables.

In addition, you can set variables (and also basically every other configuration setting) per build context. Those build contexts can be production, deploy-preview, or branch-deploy. This will be important later.

A netlify.toml file may look like this:

# Settings in the [build] context are global and are applied to all contexts
# unless otherwise overridden by more specific contexts.
[build]
  publish = "dist/"
  command = "npm run build"

# Deploy Preview context: all deploys generated from a pull/merge request will
# inherit these settings. We can't set these values in the UI.
[context.deploy-preview]
  environment = { SECRET = "not secret value for previews" }

The Actual Exploit

Assume a scenario where you have a public repo that Netlify builds. Within the repo, you have a lambda function that consumes a secret key to access an external service. We wouldn’t want to commit those secret keys in the repo so we set them through the UI on Netlify’s website.

A bad actor can then come along and submit a PR that uses (and prints) your environment variables on the front-end; this malicious code will be automatically deployed by Netlify, which will expose the key.

But wait, can’t I use a build context to prevent those keys from being leaked?

Sort of. You’re able to specifiy a deploy preview-specific environment variables in your configuration file, which can override the secret keys in your production context. However, a bad actor can simply comment out those lines in your netlify.toml that sets those context-specific environment variables, effectively bypassing those values. As mentioned previously, the build will then fall back to the environment variables set by your site (the UI). At the time of writing, there is not way to set context-specific environment variables through the UI, so it will expose the production keys.

Can I see it in action?

This repo was set up as a Netlify website with all the default settings; it’s a very simple site that uses webpack to generate some front-end JS that consumes (during the build) and prints (when the front-end loads) the CONTEXT and SECRET environment variables. The only change to the site’s configuration on the website was within the build settings, where SECRET was set. In addition, we have a netlify.toml configuration which sets the deploy preview variables to mask our prod variables.

You can view the production site here.

Now, a non-malicious PR was submitted, which generated a deploy preview. Notice that our secret is now the value we set in the configuration file.

Finally, a malicious PR was submitted, which commented out the deploy preview settings in our configuration file. The deploy preview now exposes the production secret.

The scope seems really narrow

It is. It requires a project to be public, deploy previews to be on (it is by default), and also for the project to consume secret keys. But, given that combination, you’re (very unlikely, but still) in for a bad time.

Or you could simply turn off deploy previews.

Timeline

  • Oct 8 2019 – Proof of concept submitted to Netlify
  • Oct 9 2019 – Issue acknowledged, potential fix cited
  • Nov 3 2019 – Full writeup submitted to dev team
  • Mar 26 2020 – Fix deployed as sensitive variable policy