IS NETLIFY SAFE? BUILD LOGS, REDIRECT RULES & FUNCTIONS AUDIT
Netlify handles HTTPS, deploys, and edge. Your app still owns auth, env exposure, and function security — the places AI-generated sites commonly leak.
SCAN YOUR NETLIFY SITE NOW
Enter your Netlify URL — we check for exposed env in the bundle, open functions, and auth gaps on the live site.
JAMstack Security Model
Netlify’s JAMstack approach (JavaScript, APIs, Markup) reduces attack surface by pre-building static assets. Most server-side vulnerabilities — SSRF against an app server, SQL injection via a runtime form handler, RCE via a vulnerable backend — simply don’t apply because there is no long-running app server. The catch is that the moment you reach for Functions, Forms, Identity, or Edge Functions, you’re back in the same threat model as any other serverless platform, just with Netlify-specific defaults.
The four leak categories we keep finding on production Netlify sites: env vars printed in build logs, open redirect rules in _redirects, Functions deployed without auth, and Edge Functions that mutate headers in ways that bypass downstream checks. None of these are platform bugs. All four are configuration choices that look fine on a first read.
A useful mental model: the CDN and TLS layer are Netlify’s job. The trust boundary of every request that hits a Function, Form endpoint, or rewrite is yours. AI generators (Lovable, Cursor, Claude Code, bolt templates) reliably produce a working netlify.toml and a couple of Functions that “work in preview” — and almost never produce the auth middleware, env context scoping, or redirect allowlist that production requires.
Security Considerations
Netlify Functions
Netlify Functions are AWS Lambda under the hood. The function itself runs in an isolated environment, but the handler is whatever you wrote. Common failures we see:
- The function reads
event.headers['x-forwarded-for']and trusts it for rate limiting. Spoof the header, bypass the rate limit. Usecontext.ipfrom the new Functions API (orevent.headers['x-nf-client-connection-ip']on older runtimes). - The function exposes a “test” endpoint (
/.netlify/functions/debug) that dumps env vars or runs arbitrary SQL. The “we’ll remove it later” endpoint that ends up indexed by Google. - The function returns the result of
JSON.stringify(process.env)in an error path because the developer wanted to “see what’s wrong in prod”. Now everyone can see what’s wrong in prod. - Scheduled / Background Functions triggered by a publicly guessable path with no shared secret, so anyone can enqueue expensive work.
- Functions that accept a URL parameter and fetch it server-side (SSRF into internal Netlify metadata, cloud IMDS if misrouted, or third-party APIs billed to you).
// netlify/functions/get-user.js — wrong
exports.handler = async (event) => {
const id = event.queryStringParameters.id;
const user = await db.query(`SELECT * FROM users WHERE id = ${id}`);
return { statusCode: 200, body: JSON.stringify(user) };
};
// right
exports.handler = async (event, context) => {
const session = await getSession(event);
if (!session) return { statusCode: 401, body: '' };
const id = event.queryStringParameters.id;
if (!/^[0-9a-f-]{36}$/.test(id)) return { statusCode: 400, body: '' };
const user = await db.query('SELECT id, email FROM users WHERE id = $1', [id]);
// ownership — auth alone is not enough
if (user?.owner_id !== session.userId) return { statusCode: 403, body: '' };
return { statusCode: 200, body: JSON.stringify(user) };
};
Enumerate every path under netlify/functions/ and netlify/edge-functions/ before launch. For each: who can call it, what env it needs, what it returns on error. If the answer to “who” is “anyone with the URL,” treat that as a ship blocker unless the handler is intentionally public and rate-limited.
Form Submissions
Netlify Forms are convenient and the spam protection is opt-in. Without honeypot or reCAPTCHA, a public form will start receiving spam within hours of going live, and Netlify will rate-limit your submission inbox. Worse, if you forward submissions to email or Slack, you’re now forwarding attacker payloads into a UI that might render them.
<form name="contact" method="POST" data-netlify="true" data-netlify-honeypot="bot-field">
<input type="hidden" name="form-name" value="contact" />
<p hidden><label>Don't fill this out: <input name="bot-field" /></label></p>
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
Treat anything submitted via Forms as untrusted input even when it lands in your Slack — sanitize before rendering downstream. If you pipe form webhooks into Zapier/Make, assume the payload can contain HTML, JS, and CSV formula injection (=HYPERLINK(...) in a cell that your spreadsheet will execute when opened). Strip or quote leading =, +, -, @ characters before any spreadsheet sink.
Also audit Form notification emails: if the subject line includes a user-controlled field, you can get mail-header injection or social-engineering subjects that look like internal alerts. Keep subjects static; put user content only in the body, escaped.
Environment Variables
Netlify env vars can be scoped per context (production, deploy-preview, branch-deploy) and per branch. The default UI nudges you to set everything for “All scopes,” which means your Stripe live key is available in every Pull Request preview. Anyone with the deploy URL can hit the function and use the production key.
# Scope a secret to production only
netlify env:set STRIPE_SECRET_KEY sk_live_xxx --context production
netlify env:list
Build-time variables prefixed with frameworks’ public prefixes (NEXT_PUBLIC_, VITE_, PUBLIC_, GATSBY_) are bundled into the client. Treat them as public.
A practical split that survives AI generators:
| Variable type | Context | Notes |
|---|---|---|
STRIPE_SECRET_KEY, DB URLs, service roles |
production only |
Never All scopes |
| Test Stripe / sandbox DB | deploy-preview, branch-deploy |
Separate accounts |
NEXT_PUBLIC_* / VITE_* publishable keys |
All contexts OK | Must be safe if leaked |
| Analytics write keys | Prefer production; check product docs | Some “public” keys still allow data pollution |
Also watch Linked branch env and Shared environment variables on team sites — a junior cloning a site can inherit more secrets than the UI suggests. After any contractor offboarding, rotate secrets that ever lived in All scopes.
Build Logs
Build logs are visible to every team member and survive after deploys. A printenv or env line in your build script will dump every secret in plaintext to that log, where it stays. Audit your build commands and any custom plugins for printenv, env, set, and console.log(process.env.
Less obvious leaks:
npm installscripts that echo configuration for “debug.”- Webpack/Vite plugins that dump
process.envinto astats.jsonartifact. - Failed builds that re-print the full command line including inline secrets (
API_KEY=... npm run buildin a custom command). - Build plugins from the Netlify marketplace that request broad env access for convenience.
Treat build logs as a shared secret store. If a secret ever appeared there, rotate it — log retention means “delete the line” is not enough for anyone who already cloned the log.
Deploy Previews
Deploy previews are public by default. The URL pattern is guessable per PR, and Netlify’s GitHub integration posts the link as a PR comment — so anyone watching the public repo gets it for free. Use Password Protection (Pro+) or Role-based access control (Enterprise) for projects with sensitive features.
Previews also inherit the same Function paths as production (/.netlify/functions/*). If preview env accidentally includes production Stripe or OpenAI keys, the preview is a free abuse endpoint until the PR closes. Prefer:
- Preview-scoped secrets only.
- Password protection on private product work.
- Shorter retention / auto-delete of old deploy previews.
- No production data fixtures in the static build that ships to previews.
Redirect Rules
_redirects and netlify.toml redirects are the cleanest way to ship an open redirect. The pattern that ships in production:
# _redirects — open redirect, do not use
/go /:url 302
A user visits /go?url=https://attacker.example/login and lands on the phishing page wearing your domain in the referrer. Always allowlist destinations:
/go/docs https://docs.example.com 302
/go/blog https://blog.example.com 302
Same trap exists with proxy rules — /api/* https://:host/:splat 200 is a fully open SSRF if host is user-controlled.
AI-generated netlify.toml often copies SPA fallbacks and “proxy to API” blocks from tutorials:
# Dangerous shape — do not ship
[[redirects]]
from = "/proxy/*"
to = "https://:splat"
status = 200
force = true
Safe proxies fix the upstream host:
[[redirects]]
from = "/api/*"
to = "https://api.internal.example.com/:splat"
status = 200
force = true
headers = {X-From = "netlify"}
Even then, ensure the origin authenticates the caller — a blind proxy that forwards Authorization is fine; a proxy that strips auth headers because an Edge Function “normalized” them is a silent public API.
Identity, gated sites, and soft gates
Netlify Identity (and password-protected sites) are soft gates for static assets. Anyone who obtains a deploy artifact, an old CDN cache, or a leaked preview URL can still read built HTML/JS. Put real secrets behind Functions with session checks; do not put Stripe live keys in the static bundle behind a “members only” gate and call it done.
If you use Identity JWT in Functions, validate the token signature and expiry server-side. Never trust a client-supplied user_id claim without verification.
Common Mistakes We See in Audits
- Production secrets duplicated to “All scopes,” exposing them in deploy previews.
- Functions endpoints without any auth check, discovered via
/.netlify/functions/URL fuzzing. _redirectsproxy rules with placeholder hosts that effectively act as open proxies.- Build logs containing
npminstall output that printed env vars due to a misbehaving postinstall script. - Edge Functions that strip
Authorizationheaders when forwarding to origin, defeating the origin’s auth. - Forms webhooks that auto-post to Slack without sanitization, enabling markdown/link spam and token phishing.
- Background Functions used as free compute by unauthenticated callers (cost attack, not just data leak).
- Security headers only on
/while/app/*or Function responses omit them. - SPA
_redirects/* /index.html 200combined with sensitive JSON files in/publicthat should have been gitignored.
Comparison vs Vercel
Both platforms have similar SOC 2 posture and similar default exposure. The differences:
- Netlify’s
_redirectsis plain text and easy to misread. Vercel’svercel.jsonis structured but easier to over-permission. - Netlify Forms is built-in and unique to the platform. Spam protection is opt-in.
- Vercel’s Deployment Protection is more polished. Netlify’s Site Password is older and clunkier.
- Both ship
NEXT_PUBLIC_*/VITE_*env vars to the browser. Both leak secrets the same way when developers forget. - Netlify’s function URL layout (
/.netlify/functions/name) is a stable recon target; Vercel routes often hide under/api/*app structure but are equally public without auth. - Edge middleware on both platforms is a common place AI code “fixes CORS” by reflecting
Origin.
If you are choosing purely on security defaults, neither wins by magic — the team that scopes env and auths every function wins.
Enterprise Considerations
- SSO: SAML SSO is Enterprise-only. Below that, every team member has a personal account.
- Audit Logs: Team Audit Log is on Pro+; full deployment and CLI activity is Enterprise.
- SOC 2 boundary: Netlify covers the platform. Your application’s handling of customer data is yours.
- Secret rotation: No built-in rotation. Use Doppler, Infisical, or rotate via the Netlify API on a schedule.
- RBAC: Role separation for who can edit env vars vs who can deploy matters — a compromised GitHub OAuth session with deploy rights is enough to ship a malicious Function.
- GitHub/GitLab integration scopes: Review which repos the Netlify app can access; least privilege per monorepo.
Security Assessment
Strengths
-
- Enterprise-grade CDN and infrastructure
-
- Automatic HTTPS with Let’s Encrypt and managed certs
-
- SOC 2 Type II compliance
-
- Built-in DDoS protection
-
- Encrypted environment variables with per-context scoping
-
- Deploy previews with optional access controls
-
- Forms with built-in (opt-in) spam protection
-
- Edge Functions for request-time controls (when written carefully)
-
- Strong static-hosting posture vs a DIY VPS
Concerns
-
- Netlify Functions security is developer responsibility
-
- Default “All scopes” env var setting leaks production secrets to previews
-
_redirectsand proxy rules trivially become open redirects / SSRF
-
- Build logs can leak secrets and are visible team-wide
-
- Form submissions need explicit spam protection
-
- Deploy previews are public by default
-
- Identity/password gates are not API security
-
- AI-generated config ships the four failure modes above at high frequency
Edge Functions and identity
Edge Functions can rewrite headers and responses before the CDN origin. AI-generated edge code sometimes:
- Strips
Authorizationwhen proxying to an API (accidentally public). - Reflects
OriginintoAccess-Control-Allow-Originwithout allowlisting. - Adds debug headers that leak internal hostnames.
- Sets cookies without
Secure/HttpOnly/SameSiteon auth bridges. - Implements “auth” by checking a shared query param (
?key=demo) that ends up in logs and Referers.
Identity / gated sites: treat member-only static pages as soft gates — anything in the build output can be scraped. Put real secrets behind Functions with session checks.
Minimal Edge pattern for origin allowlist (illustrative):
// netlify/edge-functions/cors.js
const ALLOW = new Set(["https://app.example.com", "https://www.example.com"]);
export default async (request, context) => {
const origin = request.headers.get("Origin");
const response = await context.next();
if (origin && ALLOW.has(origin)) {
response.headers.set("Access-Control-Allow-Origin", origin);
response.headers.set("Vary", "Origin");
}
return response;
};
Never use Access-Control-Allow-Origin: * with credentialed APIs.
AI generator pitfalls on Netlify
- Vite/React apps with
VITE_OPENAI_KEYin the client build. - Functions copied from tutorials without auth.
_redirectsproxy to a user-controlled host (SSRF / open proxy).- Forms without honeypot → Zapier spam + webhook payload abuse.
- Deploy previews for a public repo advertising admin UI paths.
- Build plugins that print env for “troubleshooting.”
- “Temporary”
console.log(event)in production Functions that dump Authorization headers into log drains. - Scaffolded
netlify.tomlthat enables pretty redirects for marketing pages and accidentally opens/admin → /admin.htmlwithout app auth. - Dual deploy of the same secret as both a Netlify env var and a hard-coded fallback in source “so local works.”
When the generator says “I set up Netlify Forms and a contact Function,” assume both need a second human pass: spam controls on the form, auth and validation on the Function, no shared production secrets on the preview context.
Split of responsibility: Netlify vs you
| Layer | Netlify owns | You own |
|---|---|---|
| TLS, CDN, DDoS | Yes | Config custom domains correctly |
| Build isolation | Yes | Don’t print secrets; audit plugins |
| Function runtime isolation | Yes | Auth, validation, SSRF, cost controls |
| Env encryption at rest | Yes | Scope, rotation, least privilege |
| Forms storage | Yes | Spam, webhook trust, rendering |
| Redirect engine | Yes | Allowlists, no open proxies |
| App authorization / BOLA | No | Entirely yours |
Hardening checklist
- Scope secrets with
netlify env:set ... --context production. - Auth on every Function; validate IDs; parameterized queries; ownership checks.
- Allowlist-only redirects; no
:urlopen patterns. - Honeypot + CAPTCHA on Forms; sanitize webhook sinks.
- Password-protect previews for sensitive apps.
- Security headers via
_headersornetlify.toml. - No
printenvin build; audit postinstall scripts; rotate anything that leaked to logs. - Edge Functions: allowlist CORS; never strip auth headers by accident.
- Inventory
/.netlify/functions/*and remove debug handlers. - Scan production URL with VibeEval, Token Leak Checker, Security Headers Checker.
How to verify
netlify env:list
# Inspect context columns — production-only secrets
# Open redirect negative test
curl -sI "https://yoursite.com/go?url=https://evil.test" | grep -i location
# Must not redirect to evil.test
# Function without session
curl -sI "https://yoursite.com/.netlify/functions/get-user?id=1"
# Expect 401
# Headers present on HTML and API
curl -sI "https://yoursite.com/" | grep -iE 'strict-transport|content-security|x-frame'
curl -sI "https://yoursite.com/.netlify/functions/health" | grep -iE 'x-content-type|referrer-policy'
Also open a deploy preview as an anonymous browser and confirm production payment keys are not callable from preview Functions. If preview can charge live Stripe, your context scoping failed.
The Verdict
Netlify is a safe deployment platform with excellent infrastructure security. The JAMstack model meaningfully reduces server-side attack surface compared to traditional hosting. The risks live in the four places where your config meets Netlify’s defaults: Functions auth, env var scoping, redirect rules, and Preview exposure. Get those right and the platform takes care of the rest.
For AI-assisted sites, add a fifth habit: treat every generated netlify.toml and Function as untrusted until the ten-item checklist above is green. The platform will not refuse an insecure config that still deploys cleanly.
Related Resources
How to Secure Netlify
Step-by-step guide covering env var scoping, Functions auth patterns, redirect rule auditing, and Preview protection.
Netlify Security Checklist
Interactive checklist for the four launch-blockers above plus the quarterly review items.
Is Vercel Safe?
Side-by-side comparison of the most common deployment failure modes on the two leading JAMstack platforms.
Security Headers Checker
Grade _headers / netlify.toml output.
_headers baseline for Netlify
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'
Tune CSP for Stripe.js / analytics. Apply similar headers on Function responses when you return HTML or JSON that browsers interpret loosely.
Bolt and AI templates on Netlify
Bolt.new and many AI scaffolds default to Netlify. Expect:
- Functions under
/.netlify/functions/*without auth VITE_*secrets in the static build- SPA fallback
/* /index.html 200that accidentally serves sensitive files from/public - Preview deploys with production Stripe if env scope is “All”
Use the same four-check model (Functions, env, redirects, previews) after every generator session. See Bolt security and How to Secure Netlify.
Incident notes: function cost abuse
Unauthenticated Functions that call OpenAI or generate images are a billing weapon. Attackers script /.netlify/functions/generate until your quota dies. Mitigations: auth, per-user rate limits, provider spend caps, and alerts on invocation count — not only data confidentiality.
Scan Your Netlify Site
Let VibeEval scan your Netlify deployment for security vulnerabilities — including the open-redirect, Functions-without-auth, and exposed-env-var patterns that account for most incidents.
COMMON QUESTIONS
SECURE BEYOND NETLIFY DEFAULTS
Platform security is not app security. Scan the live site for keys, open endpoints, and access-control gaps Netlify cannot fix for you.
14-day free trial · No credit card · Cancel anytime