VibeSecurity

Platform checklist

Cloudflare Workers security checklist for AI-built apps

A Worker is a small program that answers HTTP requests from the public internet. AI tools generate them quickly, often with a key written into the config file and no check of who is calling. By the end of this checklist you will have confirmed where your secrets live, which addresses serve your Worker and what each route does for a stranger.

By the VibeSecurity team6 min read

Where the trust boundary is

Everything inside the Worker is server-side code, so it may hold secrets and talk to databases through bindings. Everything that reaches it is untrusted: the URL, headers, cookies and body of each request. A Worker has no login screen in front of it unless you add one, so each route is public until your code says otherwise.

Configuration is part of the boundary. Cloudflare's documentation says secrets are environment variables whose values are not visible in Wrangler or the dashboard after you define them, and it says not to use vars to store sensitive information in your Worker's Wrangler configuration file. That file is normally committed to git, so a key placed under vars is published with your code.

If the Worker serves a front end as well, the usual rule applies to that part: anything sent to the browser is public.

The checklist

Cloudflare Workers review areas
AreaWhat to verifyHow to test on your own projectPass condition
Secrets and varsNo credential sits under vars in the Wrangler configuration file or in source.Read wrangler.toml or wrangler.jsonc and run the searches in Step 1.Only non-sensitive values are under vars, and credentials are set as secrets.
Local secret files.dev.vars and .env files are ignored by git and were never committed.Check .gitignore and search history with the commands in Step 1.Both patterns are ignored and history has no hits.
AddressesYou know every address that serves the Worker.Read workers_dev and preview_urls in the config, then request the workers.dev address with curl.Only intended addresses respond, and the production domain is a custom domain or route.
AuthenticationPrivate routes verify the caller inside the fetch handler.Call each route with curl and no credentials, on every address the Worker answers on.Private routes refuse on all of them.
Cross-user accessRecords are scoped to the verified user.As a second test user, request the first user's record id.The second user gets a refusal or an empty result.
D1 queriesSQL uses prepared statements with bound parameters.Search for prepare( calls that build strings, using the command in Step 3.No query joins request input into the SQL text.
Abuse limitsRoutes that cost money or send messages are limited per user.Call the route repeatedly from a script against your own Worker.Requests beyond your limit get a 429 response.
Global stateNo per-request data is kept in module-level variables.Read the top of each module for let or mutable objects that hold user or request data.Request data is passed through function arguments only.

Step 1: Secrets, vars and local files

Cloudflare documents npx wrangler secret put for adding a secret to a deployed Worker, and .dev.vars or .env files for local development. It says those files should not be committed to git and tells you to add .dev.vars* and .env* to your .gitignore. Run the searches below on your own project, move anything sensitive out of vars, and rotate any credential that was ever committed.

Config and history checks
grep -nA12 "vars" wrangler.toml wrangler.jsonc 2>/dev/null
grep -nE "\.dev\.vars|\.env" .gitignore
git ls-files | grep -E "\.dev\.vars|^\.env"
git log --all -p -S"sk_live_" | head -50

npx wrangler secret list
npx wrangler secret put STRIPE_API_KEY

Step 2: Know every address

A Worker can answer on a workers.dev subdomain, on preview URLs and on your own domain. Cloudflare's documentation recommends running production Workers on a Workers route or custom domain, not on the workers.dev subdomain, and shows workers_dev = false in the Wrangler configuration file to disable that route. It says preview URLs are available publicly when enabled, that they default to matching your workers_dev setting, and that preview_urls = false turns them off. It also describes using Cloudflare Access to restrict previews.

This matters when protection sits in front of your custom domain, for example rules on your zone, while the same code is reachable directly on another address. Confirm in your own project which addresses respond. If a route is private, it should refuse on all of them because the check is in the code.

wrangler.toml settings and an outside probe
workers_dev = false
preview_urls = false

curl -i "https://your-worker.your-subdomain.workers.dev/api/private"
curl -i "https://your-domain.example/api/private"

Step 3: The fetch handler, D1 and rate limits

Read the fetch handler as a list of routes and ask of each one: who may call this, and where does the code check? Generated Workers often verify nothing, or compare a header with a secret using ordinary equality. Cloudflare's best practices recommend crypto.subtle.timingSafeEqual() for comparing secrets, and the Web Crypto API instead of Math.random() for anything security-sensitive such as tokens.

For D1, the documentation says binding parameters with bind() to prepared statements prevents SQL injection attacks. For abuse, Cloudflare provides a rate limiting binding. Its documentation advises against using IP addresses as the key, because many users may share one, and suggests stable identifiers such as user ids. It also describes the limiter as permissive and eventually consistent, not an accurate accounting system, so treat it as protection against floods and keep hard spending caps with your paid providers too.

Bound parameters and a per-user limit
grep -rnE "prepare\(`.*\$\{|prepare\(.*\+" src

const row = await env.DB
  .prepare("SELECT id, title FROM notes WHERE id = ? AND owner_id = ?")
  .bind(noteId, userId)
  .first()

const { success } = await env.MY_RATE_LIMITER.limit({ key: userId })
if (!success) {
  return new Response("Too many requests", { status: 429 })
}

Step 4: Global state and CORS

Cloudflare's best practices explain that Workers reuse isolates across requests, so a variable set during one request is still present during the next. Generated code that stores the current user or a request body in a module-level variable can hand one visitor's data to another. Pass state through function arguments instead.

Check the CORS headers the Worker sends. AI tools often add a wildcard origin to clear a browser error. A wildcard is fine for a truly public, read-only API. It is the wrong answer for routes that use cookies or return private data, where the allowed origin should be your own site.

Common mistakes

  • Putting an API key under vars in the Wrangler configuration file and committing it.
  • Committing .dev.vars because only .env was in .gitignore.
  • Protecting the custom domain and forgetting the workers.dev address serves the same code.
  • Leaving public preview URLs on for a Worker bound to production data.
  • Building SQL with template strings from request input.
  • Keeping the current user in a global variable.
  • Leaving an AI or email route open with no limit per user.

Keep it working

Re-read the Wrangler configuration file in every change that touches it, and repeat the outside probes after each deploy on every address. Pass condition: no credential in config, source or history, private routes refuse on all addresses, queries use bound parameters, and costly routes return 429 when hammered.

Frequently asked questions

Are Cloudflare Workers secure?

The platform runs your code, and your code decides who gets what. A Worker has no authentication unless you add it, so every route is public until the fetch handler checks the caller. Secrets, addresses and query handling are the areas to review, and this checklist covers each.

What is the difference between vars and secrets in Cloudflare Workers?

Both reach your code as environment variables. Cloudflare's documentation says the difference is that secret values are not visible in Wrangler or the dashboard after you define them, and it says not to use vars to store sensitive information in the Wrangler configuration file.

Is my workers.dev URL public?

Yes, if it is enabled. Anyone with the address can send requests to it. Cloudflare recommends running production Workers on a route or custom domain and documents workers_dev = false to disable the subdomain. Check your own configuration and test the address with curl.

Are Cloudflare preview URLs private?

No. Cloudflare's documentation says that when enabled, preview URLs are available publicly. You can turn them off with preview_urls = false or restrict them with Cloudflare Access.

How do I prevent SQL injection in Cloudflare D1?

Use prepared statements with bound parameters. Cloudflare's D1 documentation says binding parameters with bind() prevents SQL injection attacks. Never join request input into the SQL text.

Sources

  1. 1.Cloudflare docs: Workers secrets
  2. 2.Cloudflare docs: workers.dev routing
  3. 3.Cloudflare docs: Preview URLs
  4. 4.Cloudflare docs: Workers best practices
  5. 5.Cloudflare docs: Rate Limiting binding
  6. 6.Cloudflare docs: D1 prepared statements
  7. 7.MDN: Cross-Origin Resource Sharing (CORS)