VibeSecurity

Platform checklist

Netlify security checklist for AI-built apps

Netlify serves a static front end and adds functions and forms on the side, which suits AI-built sites. The security risks sit in the pieces around the static files: variables exposed at build time, public function endpoints, open previews and spam-prone forms. This checklist covers each and ends with checks you can repeat.

By the VibeSecurity team4 min read

Where the trust boundary is

Static files and anything bundled into them are public. Netlify Functions are public endpoints by default: they receive a standard Request and any authorization is your code's job. Environment variables can be scoped to Builds, Post processing and Functions, and only the Functions scope keeps a value out of build output.

See our guides on client-side secrets and API authentication for the concepts. This page covers how to check them on Netlify.

The checklist

Netlify review areas
AreaWhat to verifyHow to test on your own projectPass condition
Environment variablesSecrets have only the Functions scope and are flagged as secretRun netlify env:list; check scopes in Project configurationNo secret has Builds scope unless a build truly needs it
ContextsProduction, Deploy Preview and Branch deploy use different valuesReview per-context values and netlify.tomlPreviews use test credentials
Deploy previewsSite protection is on and the sensitive variable policy is understoodOpen a preview in a private window; open a fork PRLogin prompt, or approval required for untrusted PRs
HeadersSecurity headers are set for static filescurl -I the production URLnosniff, frame protection and CSP present
FunctionsEach function checks the caller and validates inputCall each function with no tokenPrivate functions return 401 or 403
FormsHoneypot or reCAPTCHA on every form; no sensitive data collectedRead the form markup; submit a bot-like requestSpam is rejected

Step 1: Environment variables and scopes

Variables declared in netlify.toml get Builds and Post processing scope by default, so anything secret belongs in the site settings or CLI with only the Functions scope. A value with Builds scope can end up inlined into your bundle by your bundler.

Values set in netlify.toml take priority over site variables, then team-level shared variables, so check both places when a value is not what you expect.

Inspect variables and scan the build output
netlify env:list
netlify env:list --plain --context production
grep -rnE "sk_live_|service_role|BEGIN PRIVATE KEY" dist build

Step 2: Deploy previews and contexts

Deploy Preview and Branch deploy contexts can hold different values from Production, and they should. Give them test keys.

For pull requests from forks and outside contributors, Netlify's default sensitive variable policy requires approval from a site member before the build starts. The other options are to deploy without sensitive variables or with no restrictions. Keep the default unless you have a reason. Site members' own commits always deploy without restriction.

To keep previews from being public, Netlify offers basic password protection on Pro plans and above, and team login with single sign-on on Enterprise plans. Check which applies to your plan.

Step 3: Headers

Add a _headers file in the publish directory or a headers block in netlify.toml. Netlify documents that custom headers apply only to static files served from its own store, not to proxied content, functions or server-rendered pages. Those responses must set their own headers.

netlify.toml
[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Permissions-Policy = "geolocation=(), microphone=(), camera=()"

Step 4: Functions

Every function is reachable at its path, either the default /.netlify/functions/name or a custom path from its config. Read secrets with Netlify.env.get inside the handler, verify a token or session before doing anything privileged, and derive the user from the verified token rather than the request body. Add rate limiting for functions that call paid APIs.

Function with an auth check
export default async (req: Request) => {
  const token = req.headers.get("authorization")?.replace("Bearer ", "");
  if (!token || !(await verifyToken(token, Netlify.env.get("JWT_SECRET")!))) {
    return new Response("Unauthorized", { status: 401 });
  }
  return new Response("ok");
};

Step 5: Forms

Netlify detects forms by the data-netlify attribute and a form name. Submissions are filtered by Akismet automatically, and you can add a honeypot field or reCAPTCHA on top. For AJAX submissions, include the honeypot field in the POST body or it will not work. Do not collect sensitive personal or health information through a basic contact form.

Form with a honeypot
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
  <p hidden><label>Leave empty <input name="bot-field" /></label></p>
  <input name="email" type="email" required />
  <button type="submit">Send</button>
</form>

Step 6: Verify from outside

Headers should show on the page. The function call without a token should return 401 or 403. Read recent function logs and remove anything that prints tokens or request bodies.

Outside checks against your own site
curl -sI https://your-site.example.com | grep -iE "x-frame|x-content-type|strict-transport"
curl -i -X POST https://your-site.example.com/.netlify/functions/your-function
netlify logs function

Common mistakes

  • Putting API keys in netlify.toml, which gives them Builds scope.
  • Expecting _headers to protect function responses.
  • Using production credentials in Deploy Preview contexts.
  • Assuming a function is private because the front end never links to it.
  • Shipping forms with no honeypot or CAPTCHA.
  • Turning off the untrusted deploy approval setting to speed up pull requests.

Keep it working

Re-run the variable listing, header check and no-token function calls after every change to netlify.toml or a function. Review each new function for its own authentication.

Frequently asked questions

Are Netlify Functions private?

No. They are public endpoints by default, reachable at their URL by anyone. Add your own authentication and authorization checks inside each function, and treat every input as untrusted.

Why is my secret showing up in the built site?

Most likely it has Builds scope and your bundler inlined it, or it is declared in netlify.toml, which defaults to Builds scope. Move it to Functions scope only, rotate it, and read it at runtime with Netlify.env.get.

Do _headers rules apply to functions?

No. Netlify says custom headers apply to static files it serves, not to proxied content or functions. Have functions and server-rendered responses set their own headers.

Should deploy previews be protected?

If they use real data or unreleased work, yes. Use site protection where your plan allows, use test credentials in preview contexts, and keep approval on for untrusted pull requests.

Sources

  1. 1.Netlify docs: Environment variables overview
  2. 2.Netlify docs: Custom headers
  3. 3.Netlify docs: Functions overview
  4. 4.Netlify docs: Forms spam filters
  5. 5.Netlify docs: Site protection