secure-by-default
Skill2 starsChanged 40 days agoMITView on GitHub
Write authentication, database access, secret handling, payments, file uploads, and LLM endpoints securely by default. Use whenever adding or editing login or sessions, Supabase or Firebase queries, API routes or Server Actions, environment variables or API keys, Stripe or checkout code, upload handlers, CORS, redirects, or a route that calls an LLM, even when the user does not mention security.
- Reads credentials
1 other project uses a version of this.
Adopted by
Most recently taken up by
- contactandrewchl-wq/turtle-mcptheir own version, first seen Nov 15, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- contactandrewchl-wq/turtle-mcp★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository2 other files it gives its agents.
- live-exposure-checkSkill
- preflight-auditSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
15 rules. About 0 are common, 8 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
Never trust the request body for price, role, quantity, owner ids or verification flags.
- Distinctive: line 21
Verify every payment webhook signature and fail closed when the secret is unset.
- Distinctive: line 22
Treat user input and model output as untrusted at every sink: HTML, SQL, shell, URL, file path.
- Distinctive: line 25
When a law blocks the quick fix, fix the cause:
- Very specific: line 34
Never read user_metadata in a policy.
- Very specific: line 37
Every query by an id from the request also filters by owner (where: { id, userId: session.userId }) or checks the row's owner before …
- Very specific: line 40
Write allow-listed fields only: const { title, body } = input, then set user_id from the session.
- Very specific: line 47
Never create a Supabase client with service_role or sb_secret_ in browser or app code, and never call supabase.auth.admin from the client.
- Very specific: line 49
Read secrets through a loader that throws when the variable is missing: no || 'secret' or ??
- Distinctive: line 59
Every route that costs money (model calls, email, SMS, images) requires auth, a per-user rate limit and input size caps.
- Very specific: line 65
Check file type from the content (magic bytes), allow-list extensions, re-encode images and cap the size (multer limits.fileSize).
- Very specific: line 67
Keep user files private (private bucket, per-user Storage rules) and serve them through signed URLs with Content-Disposition: attachment …
- Distinctive: line 74
Call the model from the server with a server-only key.
- Distinctive: line 76
No keys, connection strings or access rules in the system prompt.
- Distinctive: line 78
If one agent holds private data, reads untrusted content (web pages, emails, uploads, other users' text) and can act outward (send, post, …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Paths in this file are relative to this skill's folder.
Secure by default
Apply these rules, unasked, to every line you write or edit that touches auth, data, secrets, payments, uploads, outbound requests or a model call. It is cheap now and expensive to retrofit.
Six laws
- The browser is public. A secret, a price or an auth decision that reaches the client is already broken.
- Deny by default. Every table, row, route and Server Action checks "is this this user's", not just "is someone logged in".
- Never trust the request body for price, role, quantity, owner ids or verification flags. Take the user id from the verified session.
- Verify every payment webhook signature and fail closed when the secret is unset.
- Treat user input and model output as untrusted at every sink: HTML, SQL, shell, URL, file path.
- Secrets live only on a server you control, never behind a public env prefix.
When a law blocks the quick fix, fix the cause:
- An env var undefined in the browser means the call belongs on the server. Do not add
NEXT_PUBLIC_orVITE_. - A CORS error means add the one origin that needs access, not every origin.
- An empty Supabase result means write the right policy, not
using (true)or the service key. - An SDK that refuses to run in the browser is right. Do not set
dangerouslyAllowBrowserfor a key you ship.
Auth and data access
- Supabase: in the same change that creates a table, add
alter table <t> enable row level security;and one policy per operationto authenticated, withusing ((select auth.uid()) = user_id)andwith check ((select auth.uid()) = user_id)on insert and update. Useusing (true)only on truly public, read-only tables with no private columns. - Never read
user_metadatain a policy; users can edit it. Keep roles inapp_metadata, a roles table the user cannot write, or Firebase custom claims (request.auth.token.admin == true). - A Supabase
security definerfunction lives in a schema the API does not expose and setssearch_path = ''. A function only the server calls getsrevoke execute ... from public, anon, authenticated; a grant toservice_rolealone locks nothing. Views over protected tables usewith (security_invoker = true). - Firebase rules:
allow read, write: if request.auth != null && request.auth.uid == userId;(or compareresource.data.ownerUid). Never shipif true, a barerequest.auth != nullon per-user data, or the test-mode date rule. - Every query by an id from the request also filters by owner (
where: { id, userId: session.userId }) or checks the row's owner before returning or changing it. Return 404 otherwise. - Route handlers and Server Actions are public endpoints. Validate arguments and check auth and ownership inside each one; a page, layout or middleware check does not cover them (middleware or
proxy.tsonly redirects, CVE-2025-29927). In server code usegetClaims()orgetUser(), nevergetSession(). - Admin checks run on the server against a server-side role. Client flags (
isAdmin, localStorage) only hide buttons. - Write allow-listed fields only:
const { title, body } = input, then setuser_idfrom the session. Neverinsert(req.body),Model(**data),create($request->all())or DRFfields = '__all__'.
More: references/data-and-auth.md.
Secrets
NEXT_PUBLIC_,VITE_,EXPO_PUBLIC_,REACT_APP_,PUBLIC_,next.configenv:, Vitedefineandapp.jsonextraall put the value in the shipped bundle. A secret never goes there; move the call to a server route, Edge Function or Cloud Function.- Never create a Supabase client with
service_roleorsb_secret_in browser or app code, and never callsupabase.auth.adminfrom the client. - Public by design, leave them in the client: Stripe
pk_, Supabase anon orsb_publishable_, Firebase webapiKey, Sentry DSN, PostHogphc_, reCAPTCHA and Turnstile site keys. RLS, rules and key restrictions protect them, not secrecy. - Read secrets through a loader that throws when the variable is missing: no
|| 'secret'or?? 'dev-key'fallbacks, no tutorial values. Generate signing secrets per environment (openssl rand -base64 48). - A key the user pastes goes into the git-ignored
.env, never into source; put a placeholder in.env.example. Never return or logprocess.envoros.environ.
More: references/secrets.md.
Payments and metered endpoints
- The client sends a product or plan id. The server takes the price from its own catalog (
PRICES[plan]) or a Stripe Price id. Neveramount,priceorunit_amountfrom the request. - Webhooks: read the raw body (
await req.text(), orexpress.raw()mounted beforeexpress.json()), callstripe.webhooks.constructEvent(raw, sig, secret), return 500 when the secret is unset and 400 on a bad signature. Never wrap verification inif (secret). - Grant access from the webhook on
checkout.session.completedandcheckout.session.async_payment_succeeded, only whenpayment_statusis notunpaid(ACH and SEPA complete unpaid), idempotently (unique session or event id,on conflict do nothing). A success page may call the same fulfill function after retrieving the session from Stripe, granting to itsclient_reference_id, never from client data. - Every route that costs money (model calls, email, SMS, images) requires auth, a per-user rate limit and input size caps. OTP, signup and password-reset sends also get a per-destination throttle, a resend cooldown and a CAPTCHA.
More: references/payments-and-abuse.md.
Uploads and server-side fetch
- Check file type from the content (magic bytes), allow-list extensions, re-encode images and cap the size (multer
limits.fileSize). Never trust the extension orContent-Type. - Store files under a random server-side name. Never put
originalnamein a path; if you must build one,path.basenameit and verify the resolved path stays inside the upload root. - Keep user files private (private bucket, per-user Storage rules) and serve them through signed URLs with
Content-Disposition: attachmentandX-Content-Type-Options: nosniff. Never serve user SVG or HTML inline. - Fetching a user-supplied URL: https only and a host allow-list. If any host must work, resolve DNS and reject private, loopback and link-local addresses at connect time and again after every redirect.
next/imageremotePatternslists exact hosts, never**.
More: references/uploads-and-fetch.md.
LLM endpoints
- Call the model from the server with a server-only key. Require auth, a per-user rate limit, a prompt length cap and an output token cap. The server picks the model, system prompt and tools; never take them from the request.
- Render model output as text or through a sanitizer (react-markdown without
rehype-raw, or DOMPurify). Never pass it raw todangerouslySetInnerHTML,v-html,innerHTML,eval, SQL or a shell. - No keys, connection strings or access rules in the system prompt. Assume it leaks and enforce access in code.
- Tools run with the end user's permissions (an RLS-scoped client, read-only where possible), never
service_role. Validate model-chosen arguments like user input. - If one agent holds private data, reads untrusted content (web pages, emails, uploads, other users' text) and can act outward (send, post, write, fetch a URL), break one leg: drop the tool, or require human approval before the outward action.
More: references/llm-endpoints.md.
Queries, rendering, redirects and config
- SQL: placeholders only (
$1,?, Prisma tagged$queryRaw). Never interpolate input into$queryRawUnsafe,whereRaw,.extra(), f-strings or string concatenation. Sort columns and directions come from an allow-list. - Mongo: cast input to scalars (
String(x)) or schema-validate it, so an object cannot become a query operator. - Shell:
execFile,spawn([...])orsubprocess.run([...])with an argument list. Neverexec,os.systemorshell=Truewith input. - HTML: let the framework escape. Raw sinks (
dangerouslySetInnerHTML,v-html,{@html}, Blade{!! !!}, Jinja|safe,mark_safe) only take sanitized strings. - Redirects from
next,returnToorredirect: accept a path only if it starts with exactly one/and has no backslash, tab, CR or LF (browsers drop those and read\as/, so/<tab>/evil.comreaches//evil.com), else use a default. Or requirenew URL(value, base).originto equal the base origin. - CORS: an exact origin list from env. Never
origin: true, a reflectedOriginheader orallow_origins=["*"]together with credentials. - Cookies:
httpOnly,securein production,sameSite: 'lax'. Keep the framework's CSRF protection on and exempt only signature-verified webhooks; cookie-auth route handlers checkOriginor a CSRF token. - Production: debug off (
DEBUG,APP_DEBUG, Flaskdebug),NODE_ENV=production, generic error bodies, and no request bodies, auth headers, tokens or passwords in logs.
More: references/uploads-and-fetch.md (SQL, Mongo, shell, HTML) and references/payments-and-abuse.md (CSRF, redirects).
Handoff
Asked whether a built app is safe, or about to deploy: hand off to preflight-audit. Given the user's own running URL: hand off to live-exposure-check.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

