agentleFS
joaoguirunasjoaoguirunas/team-os/.claude/skills/dev-security-patterns/SKILL.md

dev-security-patterns

Skill6 starsChanged 24 days agoMITView on GitHub

Padrões de segurança para software complexo — autenticação, autorização, RLS, OWASP top 10, validação de input, secrets management e a varredura das armadilhas de código gerado por IA (segredo no cliente, RLS aberta, auth por user_metadata, prompt injection).

  • Reads credentials

The file

Files
SKILL.md

Security Patterns — Software Complexo

JWT — Autenticação

// Geração
const accessToken = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET!,
  { expiresIn: '15m' }   // curto
)
const refreshToken = jwt.sign(
  { userId: user.id },
  process.env.JWT_REFRESH_SECRET!,
  { expiresIn: '7d' }
)

// Validação em middleware
const verifyToken = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]
  if (!token) return res.status(401).json({ error: { code: 'UNAUTHORIZED', requestId: req.requestId } })
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET!)
    next()
  } catch {
    return res.status(401).json({ error: { code: 'UNAUTHORIZED', requestId: req.requestId } })
  }
}

Regras: Access token 15min. Refresh token 7 dias com rotação. Nunca localStorage — usar httpOnly cookie. Secret mínimo 256 bits.

Autorização — RBAC

const requireRole = (...roles: string[]) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: { code: 'FORBIDDEN', requestId: req.requestId } })
  }
  next()
}

// Uso
router.delete('/users/:id', verifyToken, requireRole('admin'), deleteUser)

Row Level Security (RLS — Supabase/Postgres)

-- Habilitar RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Usuário vê apenas seus dados
CREATE POLICY "user_own_orders" ON orders
  FOR ALL USING (auth.uid() = user_id);

-- Admin vê tudo — papel vem de app_metadata (só o servidor escreve)
CREATE POLICY "admin_all_orders" ON orders
  FOR ALL USING ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin');

RLS é a última linha de defesa — aplicar em todas as tabelas com dados de usuário.

  • auth.jwt() ->> 'role' (claim de topo) é o papel do Postgres (authenticated/anon), não o papel da aplicação — política escrita assim nunca casa.
  • Nunca autorizar por user_metadata (raw_user_meta_data): o próprio usuário edita esse campo pela API de auth e se dá qualquer papel. Papel de aplicação mora em app_metadata ou numa tabela de papéis.
  • "RLS ativo" não prova nada: RLS ligado sem policy nega tudo; RLS com USING (true) libera tudo. Leia a policy.
  • Bucket do Storage é tabela também (storage.objects) — USING (true) ali expõe todos os arquivos.

Validação de Input

const createUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100).trim(),
})

// Middleware
const validate = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({
      error: {
        code: 'VALIDATION_ERROR',
        details: result.error.errors,
        requestId: req.requestId,  // sempre incluir requestId
      }
    })
  }
  req.body = result.data  // dados sanitizados
  next()
}

Nunca confiar em input do cliente. Validar e sanitizar tudo que vem do exterior.

OWASP Top 10 — Checklist

RiscoMitigação
SQL InjectionORM/query builder, nunca concatenar queries
Broken AuthJWT curto, refresh rotation, httpOnly cookies
Sensitive Data ExposureHTTPS, nunca logar dados sensíveis
Broken Access ControlRBAC + RLS obrigatórios
Security MisconfigurationEnv vars para secrets, nada hardcoded
XSSSanitizar output, Content-Security-Policy
Vulnerable Dependenciesnpm audit em CI
Insufficient LoggingLogar autenticações e acessos negados com requestId

Secrets Management

# ✅ Variáveis de ambiente
DATABASE_URL=postgresql://...
JWT_SECRET=super-secret-256-bits

# ❌ Nunca hardcoded
const JWT_SECRET = "minha-chave"

Regras:

  • .env no .gitignore — nunca commitar
  • .env.example com chaves sem valores — commitar
  • Em produção: secret manager (AWS Secrets Manager, Vercel env vars)
  • Prefixo público (NEXT_PUBLIC_, VITE_, PUBLIC_, EXPO_PUBLIC_) vai para o navegador por definição — só chave publicável usa esse prefixo
  • Segredo que vazou já está queimado: apagar do código não basta. A correção só termina com a chave rotacionada no provedor

Varredura de código gerado por IA

Assistentes de código otimizam para a demo rodar. Os cinco erros abaixo são os que mais escapam em projetos Next.js + Supabase + LLM. Rode a varredura na raiz do projeto, leia cada ocorrência e classifique. Busca é triagem, não veredicto — toda ocorrência é conferida no arquivo antes de virar achado.

# Funciona em bash e zsh. Rode da raiz do projeto.

# 1. Segredo atrás de prefixo público
grep -rnE "(NEXT_PUBLIC|VITE|PUBLIC|EXPO_PUBLIC)_[A-Z0-9_]*(SECRET|SERVICE_ROLE|PRIVATE|TOKEN|PASSWORD|API_KEY)" \
  --include='*.[jt]s' --include='*.[jt]sx' --include='*.mjs' --include='*.astro' --include='*.vue' --include='*.svelte' --include='.env*' \
  --exclude-dir={node_modules,.next,.git,dist,build} .

# 2. Chave literal no código (OpenAI/Anthropic, Stripe live, JWT, AWS, chave privada)
grep -rnE "sk-(proj-|ant-)?[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]{10,}|eyJhbGciOi[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY" \
  --exclude-dir={node_modules,.next,.git,dist,build} .

# 3. service_role fora do servidor
grep -rnE "service_role|SERVICE_ROLE" \
  --include='*.[jt]s' --include='*.[jt]sx' --include='*.mjs' --include='*.astro' --include='*.vue' --include='*.svelte' \
  --exclude-dir={node_modules,.next,.git,dist,build} .

# 4. RLS aberta e autorização por user_metadata
grep -rniE "using[[:space:]]*\([[:space:]]*true[[:space:]]*\)|with check[[:space:]]*\([[:space:]]*true[[:space:]]*\)|user_metadata|raw_user_meta_data" \
  --include='*.sql' --include='*.[jt]s' --include='*.[jt]sx' \
  --exclude-dir={node_modules,.next,.git,dist,build} .

# 4. Tabelas criadas sem "enable row level security" (a saída é a lista de suspeitas)
comm -23 \
  <(grep -rhoiE "create table( if not exists)? [a-z_.\"]+" --include='*.sql' --exclude-dir=node_modules --exclude-dir=.git . | tr A-Z a-z | awk '{print $NF}' | sort -u) \
  <(grep -rhoiE "alter table( only)? [a-z_.\"]+ enable row level security" --include='*.sql' --exclude-dir=node_modules --exclude-dir=.git . | tr A-Z a-z | awk '{print $(NF-4)}' | sort -u)

# 5. Prompt de sistema montado com dado da requisição
grep -rnE "role:[[:space:]]*[\"']system[\"']|system:[[:space:]]*\`" \
  --include='*.[jt]s' --include='*.[jt]sx' --include='*.mjs' \
  --exclude-dir={node_modules,.next,.git,dist,build} .

Sem migrations SQL no repositório (schema só no painel do Supabase)? A busca 4 não enxerga nada — rode no SQL Editor: select tablename, rowsecurity from pg_tables where schemaname = 'public'; e select * from pg_policies where schemaname in ('public','storage');. Sem esse acesso, o item vira NÃO VERIFICÁVEL, nunca "ok".

#AchadoQuando é achadoSeveridade
1Segredo com prefixo públicoValor não publicável (chave de API privada, service_role, token)CRITICAL
2Chave literal no códigoQualquer chave privada no repositório. JWT literal: decodifique o payload — "role":"service_role" é CRITICAL; "role":"anon" não é achadoCRITICAL
3service_role alcançável pelo clienteArquivo com "use client", componente, hook ou módulo importado por elesCRITICAL
4aTabela com dado de usuário sem RLS, ou com USING (true) / WITH CHECK (true)Dado não é público por desenho. Sem RLS, ou UPDATE/DELETE/ALL aberto: CRITICAL. SELECT aberto (qualquer um lê): HIGH. INSERT aberto fora de formulário público: HIGHver coluna ao lado
4bAutorização por user_metadataPolicy, middleware ou checagem de papel lê user_metadataHIGH
4cBucket do Storage abertoPolicy USING (true) em storage.objects de bucket com arquivo privadoHIGH
5aEntrada da requisição no prompt de sistema e chamada com toolsreq.body/query/form chega ao system numa chamada com toolsHIGH
5bEntrada da requisição no prompt de sistema, sem toolsIdem, sem tools — heurístico, conferir à mãoMEDIUM

Não é achado (não acusar): anon key do Supabase e NEXT_PUBLIC_SUPABASE_ANON_KEY (a RLS é o portão); chave publicável do Stripe (pk_); chave de projeto de analytics (PostHog, GA); USING (true) só para SELECT em tabela pública por desenho (catálogo, posts publicados) e documentada como tal; INSERT ... WITH CHECK (true) em tabela de formulário público (lead, contato, newsletter) sem policy de SELECT/UPDATE/DELETE para anon — é o desenho normal de formulário (no máximo uma recomendação: limitar formato e tamanho no WITH CHECK ou inserir por rota de servidor com rate limit); entrada do usuário numa mensagem role: "user" separada, sem tools. Acusar o padrão seguro ensina o time a ignorar o QA.

Como reportar cada achado: linha (arquivo:linha) → o risco em uma frase ("vai para o navegador de todo visitante") → a correção em um commit. Para segredo: tipo e local, nunca o valor (mostre no máximo os 4 primeiros caracteres), e a correção sempre inclui rotacionar no provedor.

Correção só vale com nova varredura. Depois do fix, rode a varredura de novo e compare: resolvido, ainda presente, novo. Segredo só sai da lista com a rotação confirmada pelo usuário — remover do código não basta.

Rate Limiting

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,   // 10 tentativas em 15 min
  message: { error: { code: 'RATE_LIMITED', requestId: 'see-x-request-id-header' } },
})

app.use('/auth', authLimiter)
app.use('/api', rateLimit({ windowMs: 60000, max: 100 }))

Password Hashing

const hash = await bcrypt.hash(password, 12)    // custo 12 mínimo
const valid = await bcrypt.compare(input, hash)

Nunca MD5 ou SHA1 para passwords. Nunca plain text.

Logging Estruturado Seguro

O requestId gerado no middleware de entrada (ver dev-api-design) deve estar presente em todos os logs — é o fio que conecta request → serviços → banco → resposta no debugging.

// ✅ Correto — contexto rico com requestId, dado sensível ausente
logger.info({
  requestId: req.requestId,  // sempre — rastreabilidade end-to-end
  userId: user.id,
  action: 'login_attempt',
  ip: req.ip,
  success: true,
}, 'User authenticated')

logger.error({
  requestId: req.requestId,
  userId: user.id,
  action: 'payment_process',
  orderId: order.id,
  err: error,
}, 'Payment processing failed')

// ❌ Errado — sem requestId, sem contexto, pode vazar dados
console.error('Error:', error)
logger.info('Login', { password: body.password, token: jwt })

Nunca logar: passwords, tokens JWT, dados de cartão, CPF/SSN, qualquer PII desnecessário. Sempre logar: requestId, userId (não email), action, resultado (success/fail), IP em auth events.

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.