skills/aif-security-checklist/SKILL.md
Security audit checklist based on OWASP Top 10 and best practices. Covers authentication, injection, XSS, CSRF, secrets management, and more. Use when reviewing security, before deploy, asking "is this secure", "security check", "vulnerability".
npx skillsauth add lee-to/ai-factory aif-security-checklistInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Comprehensive security checklist based on OWASP Top 10 (2021) and industry best practices.
/aif-security-checklist — Full audit checklist/aif-security-checklist auth — Authentication & sessions/aif-security-checklist injection — SQL/NoSQL/Command injection/aif-security-checklist xss — Cross-site scripting/aif-security-checklist csrf — Cross-site request forgery/aif-security-checklist secrets — Secrets & credentials/aif-security-checklist api — API security/aif-security-checklist infra — Infrastructure security/aif-security-checklist prompt-injection — LLM prompt injection/aif-security-checklist race-condition — Race conditions & TOCTOU/aif-security-checklist ignore <item> — Ignore a specific check itemFIRST: Read .ai-factory/config.yaml if it exists to resolve:
paths.securitylanguage.ui for prompts, audit summaries, and next-step guidance; language.artifacts for the ignored-item state artifact; language.technical_terms for human-readable technical terminology in the ignored-item artifactIf config.yaml doesn't exist, use defaults:
.ai-factory/SECURITY.mdui_language: enartifact_language: entechnical_terms_policy: keepResolved language values:
ui_language = language.ui || "en"artifact_language = language.artifacts || language.ui || "en"technical_terms_policy = language.technical_terms || "keep"If technical_terms_policy is not one of keep, translate, or mixed, treat it as keep. Legacy values such as english also behave like keep.
All AskUserQuestion prompts, audit summaries, ignored-item explanations shown to the user, and next-step guidance MUST be written in ui_language.
The persistent SECURITY.md ignored-item artifact under paths.security MUST be written in artifact_language.
Templates and examples define structure, not fixed English output. If artifact_language is not en, translate human-readable headings, table captions, notes, ignored-item reasons when generated, and review guidance before saving. Preserve item IDs, dates, author handles, commands, paths, config keys, package names, API names, security category IDs, severity/status enum values, raw errors, and the final aif-gate-result JSON schema unchanged. Apply technical_terms_policy to other human-readable terminology.
Before running any audit, always read the resolved SECURITY.md path (default: .ai-factory/SECURITY.md). If it exists, it contains a list of security checks the team has decided to ignore.
When the user runs /aif-security-checklist ignore <item>:
When running any audit (/aif-security-checklist or a specific category):
Render this structure in artifact_language before saving. The headings below are canonical structure labels, not fixed English output; item IDs and table field meanings stay stable.
# Security: Ignored Items
Items below are excluded from security-checklist audits.
Review periodically — ignored risks may become relevant.
| Item | Reason | Date | Author |
|------|--------|------|--------|
| no-csrf | SPA with token auth, no cookies used | 2025-03-15 | @dev |
| no-rate-limit | Internal microservice, behind API gateway | 2025-03-15 | @dev |
Item naming convention — use short kebab-case IDs:
no-csrf — CSRF tokens not implementedno-rate-limit — Rate limiting not configuredno-https — HTTPS not enforcedno-xss-csp — CSP header missingno-sql-injection — SQL injection not fully preventedno-prompt-injection — LLM prompt injection not mitigatedno-race-condition — Race condition prevention missingno-secret-rotation — Secrets not rotatedno-auth-{route} — Auth missing on specific routeverbose-errors — Detailed errors exposedWhen audit results are shown, append this section at the end:
⏭️ Ignored Items (from the resolved SECURITY.md artifact)
┌─────────────────┬──────────────────────────────────────┬────────────┐
│ Item │ Reason │ Date │
├─────────────────┼──────────────────────────────────────┼────────────┤
│ no-csrf │ SPA with token auth, no cookies used │ 2025-03-15 │
│ no-rate-limit │ Internal service, behind API gateway │ 2025-03-15 │
└─────────────────┴──────────────────────────────────────┴────────────┘
⚠️ 2 items ignored. Run `/aif-security-checklist` without ignores to see full audit.
Read .ai-factory/skill-context/aif-security-checklist/SKILL.md — MANDATORY if the file exists.
This file contains project-specific rules accumulated by /aif-evolve from patches,
codebase conventions, and tech-stack analysis. These rules are tailored to the current project.
How to apply skill-context rules:
Enforcement: After generating any output artifact, verify it against all skill-context rules. If any rule is violated — fix the output before presenting it to the user.
Run the automated security audit script:
bash ~/{{skills_dir}}/security-checklist/scripts/audit.sh
This checks:
For /aif-security-checklist audits (full audit or category audit), keep the human-readable security report first and append one final fenced aif-gate-result JSON block.
Do not append this gate block for the ignore <item> writer flow unless that invocation also performs and reports an audit result.
Status mapping:
fail: an unignored critical/high security issue or other explicitly production-blocking finding remains.warn: only medium/low findings, ignored items needing review, incomplete audit evidence, or audit command limitations remain.pass: the audit completed and no unignored findings remain.Machine-readable fields:
"gate": "security"."status": "pass|warn|fail"."blocking": true|false."blockers": [."affected_files": [."suggested_next": { to /aif-fix for code/config security fixes or null when no workflow command fits.{
"schema_version": 1,
"gate": "security",
"status": "warn",
"blocking": false,
"blockers": [],
"affected_files": ["src/api/session.ts"],
"suggested_next": {
"command": "/aif-fix",
"reason": "Address non-blocking security hardening findings."
}
}
✅ Requirements:
- [ ] Minimum 12 characters
- [ ] Hashed with bcrypt/argon2 (cost factor ≥ 12)
- [ ] Never stored in plain text
- [ ] Never logged
- [ ] Breach detection (HaveIBeenPwned API)
For implementation patterns (argon2, bcrypt, PHP, Laravel) → read references/AUTH-PATTERNS.md
✅ Checklist:
- [ ] Session ID regenerated after login
- [ ] Session timeout implemented (idle + absolute)
- [ ] Secure cookie flags set
- [ ] Session invalidation on logout
- [ ] Concurrent session limits (optional)
For secure cookie settings example → read references/AUTH-PATTERNS.md
✅ Checklist:
- [ ] Use RS256 or ES256 (not HS256 for distributed systems)
- [ ] Short expiration (15 min access, 7 day refresh)
- [ ] Validate all claims (iss, aud, exp, iat)
- [ ] Store refresh tokens securely (httpOnly cookie)
- [ ] Implement token revocation
- [ ] Never store sensitive data in payload
// ❌ VULNERABLE: String concatenation
const query = `SELECT * FROM users WHERE id = ${userId}`;
// ✅ SAFE: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// ✅ SAFE: ORM (Prisma/Eloquent/SQLAlchemy)
const user = await prisma.user.findUnique({ where: { id: userId } });
// ❌ VULNERABLE: Direct user input — attack: { "$ne": "" }
const user = await db.users.findOne({ username: req.body.username });
// ✅ SAFE: Type validation
const username = z.string().parse(req.body.username);
// ❌ VULNERABLE: exec(`convert ${userFilename} output.png`);
// ✅ SAFE: execFile('convert', [userFilename, 'output.png']);
- [ ] All user output HTML-encoded by default
- [ ] Content-Security-Policy header configured
- [ ] X-Content-Type-Options: nosniff
- [ ] Sanitize HTML if allowing rich text
- [ ] Validate URLs before rendering links
// ❌ VULNERABLE: element.innerHTML = userInput; / dangerouslySetInnerHTML
// ✅ SAFE: element.textContent = userInput; / React: <div>{userInput}</div>
// ✅ If HTML needed: DOMPurify.sanitize(userInput)
// ❌ VULNERABLE: <?= $userInput ?> / {!! $userInput !!}
// ✅ SAFE: {{ $userInput }} (Blade) / htmlspecialchars($input, ENT_QUOTES, 'UTF-8')
Set CSP header: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
- [ ] CSRF tokens on all state-changing requests
- [ ] SameSite=Strict or Lax on cookies
- [ ] Verify Origin/Referer headers
- [ ] Don't use GET for state changes
csurf middleware, embed token in hidden form field and AJAX headerssameSite: 'strict', client sends token in header, server compares❌ Secrets in code
const API_KEY = "sk_live_abc123";
❌ Secrets in git
.env committed to repository
❌ Secrets in logs
console.log(`Connecting with password: ${password}`);
❌ Secrets in error messages
throw new Error(`DB connection failed: ${connectionString}`);
- [ ] Secrets in environment variables or vault
- [ ] .env in .gitignore
- [ ] Different secrets per environment
- [ ] Secrets rotated regularly
- [ ] Access to secrets audited
- [ ] No secrets in client-side code
# If secrets were committed, remove from history
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch path/to/secret-file" \
--prune-empty --tag-name-filter cat -- --all
# Or use BFG Repo-Cleaner (faster)
bfg --delete-files .env
bfg --replace-text passwords.txt
# Force push (coordinate with team!)
git push origin --force --all
# Rotate ALL exposed secrets immediately!
- [ ] API keys not in URLs (use headers)
- [ ] Rate limiting per user/IP
- [ ] Request signing for sensitive operations
- [ ] OAuth 2.0 for third-party access
- [ ] Browser/client logs are disabled in production or routed through a logger that no-ops debug output in production
- [ ] `console.log`, `console.debug`, `console.info`, and verbose client telemetry are gated by explicit non-production checks
- [ ] Production UI shows only client-safe error messages with minimal operational detail
- [ ] Raw exceptions, stack traces, SQL/ORM errors, validation library internals, upstream responses, file paths, env names, and secrets never reach UI text
- [ ] Full error details are logged server-side only, correlated with a request/error ID returned to the client
- [ ] Client-safe error payloads use stable codes/messages such as `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, or `INTERNAL_ERROR`
const isProduction = process.env.NODE_ENV === 'production';
// ✅ Client debug output is explicit and removed/no-op in production paths
if (!isProduction) {
console.debug('Form validation state', formState);
}
// ✅ Normalize unknown errors before rendering them in UI
function toClientError(error: unknown) {
if (isKnownClientError(error)) {
return { code: error.code, message: error.publicMessage };
}
return {
code: 'INTERNAL_ERROR',
message: 'Something went wrong. Try again later.',
};
}
// ✅ Validate all input with schema
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150).optional(),
});
app.post('/users', (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: {
code: 'VALIDATION_FAILED',
message: 'Some fields are invalid.',
fields: result.error.issues.map((issue) => ({
path: issue.path.join('.'),
code: issue.code,
})),
},
});
}
// result.data is typed and validated
});
// ✅ Don't expose internal errors
app.use((err, req, res, next) => {
console.error(err); // Log full error internally
// Return generic message to client
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'Something went wrong. Try again later.',
},
requestId: req.id, // For support reference
});
});
// ✅ Don't expose sensitive fields
const userResponse = {
id: user.id,
name: user.name,
email: user.email,
// ❌ Never: password, passwordHash, internalId, etc.
};
app.use(helmet()); // Sets many security headers
// Or manually:
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '0'); // Disabled, use CSP instead
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
# Check for vulnerabilities
npm audit
pip-audit
cargo audit
# Auto-fix where possible
npm audit fix
# Keep dependencies updated
npx npm-check-updates -u
- [ ] HTTPS only (redirect HTTP)
- [ ] TLS 1.2+ only
- [ ] Security headers configured
- [ ] Debug mode disabled
- [ ] Default credentials changed
- [ ] Unnecessary ports closed
- [ ] File permissions restricted
- [ ] Logging enabled (but no secrets)
- [ ] Backups encrypted
- [ ] WAF/DDoS protection (for public APIs)
For detailed race condition patterns (double-spend, TOCTOU, optimistic locking, idempotency keys, distributed locks) → read references/RACE-CONDITIONS.md
- [ ] Financial operations use database transactions with proper isolation
- [ ] Inventory/stock checks use atomic decrement (not read-then-write)
- [ ] Idempotency keys on payment and mutation endpoints
- [ ] Optimistic locking (version column) on concurrent updates
- [ ] File operations use exclusive locks where needed
- [ ] No TOCTOU gaps between permission check and action
- [ ] Rate limiting to reduce exploitation window
For detailed prompt injection patterns (direct, indirect, tool safety, output validation, RAG) → read references/PROMPT-INJECTION.md
- [ ] User input never concatenated directly into system prompts
- [ ] Input/output boundaries clearly separated (delimiters, roles)
- [ ] LLM output treated as untrusted (never executed as code/commands)
- [ ] Tool calls from LLM validated and sandboxed
- [ ] Sensitive data excluded from LLM context
- [ ] Rate limiting on LLM endpoints
- [ ] Output filtered for PII/secrets leakage
- [ ] Logging & monitoring for anomalous prompts
# Find hardcoded secrets
grep -rn "password\|secret\|api_key\|token" --include="*.ts" --include="*.js" .
# Check for vulnerable dependencies
npm audit --audit-level=high
# Find unfinished security markers
grep -rn "[T][O][D][O].*security\|[F][I][X][M][E].*security\|[X][X][X].*security" .
# Check for console.log in production code
grep -rn "console\.log" src/
# Check for verbose browser logs that need a non-production guard
grep -rn "console\.\(log\|debug\|info\|trace\)" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" src/
# Check for raw error rendering patterns in UI/client code
grep -rn "\(error\.message\|err\.message\|String(error)\|String(err)\|stack\)" --include="*.tsx" --include="*.jsx" --include="*.ts" --include="*.js" src/
# Find prompt injection risks (unsanitized input in LLM calls)
grep -rn "system.*\${.*}" --include="*.ts" --include="*.js" .
grep -rn "innerHTML.*llm\|innerHTML.*response\|innerHTML.*completion" --include="*.ts" --include="*.js" .
| Issue | Severity | Fix Timeline | |-------|----------|--------------| | SQL Injection | 🔴 Critical | Immediate | | Auth Bypass | 🔴 Critical | Immediate | | Secrets Exposed | 🔴 Critical | Immediate | | XSS (Stored) | 🔴 Critical | < 24 hours | | Prompt Injection (Direct) | 🔴 Critical | Immediate | | Race Condition (Financial) | 🔴 Critical | Immediate | | Prompt Injection (Indirect) | 🟠 High | < 1 week | | Race Condition (Data) | 🟠 High | < 1 week | | CSRF | 🟠 High | < 1 week | | XSS (Reflected) | 🟠 High | < 1 week | | Missing Rate Limit | 🟡 Medium | < 2 weeks | | Verbose Errors | 🟡 Medium | < 2 weeks | | Missing Headers | 🟢 Low | < 1 month |
Tip: Context is heavy after security audit. Consider
/clearor/compactbefore continuing with other tasks.
.ai-factory/SECURITY.md) for ignored-item state created through the ignore flow.paths.security for the ignore-state artifact, language.ui for prompts and audit summaries, language.artifacts for the ignored-item artifact, and language.technical_terms for human-readable terminology policy while deriving audit scope from repo evidence and audit commands.data-ai
Archive completed plans and roadmap milestones. Moves finished plans to the archive directory and optionally trims closed milestones from ROADMAP.md. Use when user says "archive plans", "clean up plans", "archive completed", or "trim roadmap".
tools
Set up agent context for a project. Analyzes tech stack, installs relevant skills from skills.sh, generates custom skills, and configures MCP servers. Use when starting new project, setting up AI context, or asking "set up project", "configure AI", "what skills do I need".
development
Verify completed implementation against the plan. Checks that all tasks were fully implemented, nothing was forgotten, code compiles, tests pass, and quality standards are met. Use after "/aif-implement" completes, or when user says "verify", "check work", "did we miss anything".
data-ai
Plan implementation for a feature or task. Two modes — fast (single quick plan) or full (richer plan with optional git branch/worktree flow). Use when user says "plan", "new feature", "start feature", "create tasks".