Skip to main content

security-patterns

Implements authentication, authorization, encryption, secrets management, and security hardening patterns. Use when designing auth flows, managing secrets, configuring CORS, implementing rate limiting, or when asked about JWT, OAuth, password hashing, API keys, RBAC, or security best practices.

소스 정보

저장소
CloudAI-X/claude-workflow-v2
최근 소스 활동
2026년 10월 1일 23:45
감지된 SKILL.md 언어
영어
스타
1,420
포크
191

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
security-patterns
description
Implements authentication, authorization, encryption, secrets management, and security hardening patterns. Use when designing auth flows, managing secrets, configuring CORS, implementing rate limiting, or when asked about JWT, OAuth, password hashing, API keys, RBAC, or security best practices.
# Security Patterns ### When to Load - **Trigger**: Auth flows, encryption, secrets management, CORS configuration, input validation, rate limiting - **Skip**: No security surface involved in the current task ## Security Implementation Workflow Copy this checklist and track progress: ``` Security Implementation Progress: - [ ] Step 1: Choose authentication strategy - [ ] Step 2: Implement authorization model - [ ] Step 3: Set up password hashing - [ ] Step 4: Configure secrets management - [ ] Step 5: Enable encryption (transit + rest) - [ ] Step 6: Configure CORS - [ ] Step 7: Add rate limiting - [ ] Step 8: Validate against anti-patterns checklist ``` ## Authentication Patterns ### JWT (JSON Web Tokens) ```typescript import jwt from "jsonwebtoken"; function generateTokens(user: User) { const accessToken = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET!, { expiresIn: "15m", algorithm: "HS256" }, ); const refreshToken = jwt.sign( { sub: user.id, tokenVersion: user.tokenVersion }, process.env.JWT_REFRESH_SECRET!, { expiresIn: "7d" }, ); return { accessToken, refreshToken }; } // WRONG: localStorage (XSS vulnerable) | CORRECT: httpOnly cookie for refresh, memory for access res.cookie("refreshToken", refreshToken, { httpOnly: true, secure: true, sameSite: "strict", maxAge: 7 * 24 * 60 * 60 * 1000, path: "/api/auth/refresh", }); ``` ### JWT Verification Middleware ```typescript function authenticate(req: Request, res: Response, next: NextFunction) { const header = req.headers.authorization; if (!header?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing token" }); } try { const token = header.slice(7); const payload = jwt.verify(token, process.env.JWT_SECRET!, { algorithms: ["HS256"], // pin: never let the token choose its algorithm }) as JwtPayload; req.user = { id: payload.sub, role: payload.role }; next(); } catch (err) { if (err instanceof jwt.TokenExpiredError) { return res.status(401).json({ error: "Token expired" }); } return res.status(401).json({ error: "Invalid token" }); } } ``` ### Session-Based Auth ```typescript import session from "express-session"; import { RedisStore } from "connect-redis"; app.use( session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 24 * 60 * 60 * 1000, // 24 hours }, }), ); ``` ### OAuth 2.0 / OIDC Flow Summary ``` Authorization Code Flow with PKCE (all clients): 1. Redirect to provider: /authorize?response_type=code&client_id=...&redirect_uri=...&scope=openid email &state=RANDOM&nonce=RANDOM&code_challenge=...&code_challenge_method=S256 2. User authenticates, provider redirects back with ?code=AUTHORIZATION_CODE&state=... (reject if state differs) 3. Backend exchanges code for tokens (POST /token with code_verifier, plus client_secret for confidential clients) 4. Backend validates the id_token (signature, iss, aud, exp, nonce), then creates session/JWT Public clients (SPAs, mobile): same flow without client_secret NEVER use Implicit Flow (deprecated, tokens exposed in URL) ``` ### API Key Authentication ```typescript async function authenticateApiKey( req: Request, res: Response, next: NextFunction, ) { const apiKey = req.headers["x-api-key"] as string; if (!apiKey) return res.status(401).json({ error: "API key required" }); // WRONG: Direct comparison (timing attack) | CORRECT: Hash-based lookup const hashedKey = crypto.createHash("sha256").update(apiKey).digest("hex"); const keyRecord = await db.apiKey.findUnique({ where: { hash: hashedKey } }); if (!keyRecord || keyRecord.revokedAt) return res.status(401).json({ error: "Invalid API key" }); req.apiClient = { id: keyRecord.clientId, scopes: keyRecord.scopes }; next(); } ``` ## Authorization Models ### RBAC (Role-Based Access Control) ```typescript const PERMISSIONS = { admin: [ "users:read", "users:write", "users:delete", "posts:read", "posts:write", "posts:delete", ], editor: ["posts:read", "posts:write", "posts:delete", "users:read"], viewer: ["posts:read", "users:read"], } as const; type Role = keyof typeof PERMISSIONS; function authorize(...requiredPermissions: string[]) { return (req: Request, res: Response, next: NextFunction) => { const userPermissions = PERMISSIONS[req.user.role as Role] || []; const hasPermission = requiredPermissions.every((p) => (userPermissions as readonly string[]).includes(p), ); if (!hasPermission) return res.status(403).json({ error: "Insufficient permissions" }); next(); }; } // Usage: app.delete("/api/users/:id", authenticate, authorize("users:delete"), deleteUser); ``` ### Resource-Level Authorization ```typescript // WRONG: Only checking role, not ownership -- any editor can edit ANY post // CORRECT: Check ownership or admin role app.put( "/api/posts/:id", authenticate, authorize("posts:write"), async (req, res) => { const post = await db.post.findUnique({ where: { id: req.params.id } }); if (!post) return res.status(404).json({ error: "Not found" }); if (post.authorId !== req.user.id && req.user.role !== "admin") { return res .status(403) .json({ error: "Not authorized to edit this post" }); } // WRONG: data: req.body -- mass assignment lets the client overwrite authorId const { title, body } = UpdatePostSchema.parse(req.body); // allowlisted fields only const updated = await db.post.update({ where: { id: post.id }, data: { title, body }, }); res.json(updated); }, ); ``` ## Password Handling ```typescript import bcrypt from "bcrypt"; // WRONG: plaintext or MD5/SHA256 (too fast, brute-forceable) // CORRECT: bcrypt with appropriate cost factor const SALT_ROUNDS = 12; // ~250ms on modern hardware async function hashPassword(password: string): Promise<string> { return bcrypt.hash(password, SALT_ROUNDS); } async function verifyPassword( password: string, hash: string, ): Promise<boolean> { return bcrypt.compare(password, hash); // constant-time comparison built-in } // Registration await db.user.create({ data: { email, password: await hashPassword(req.body.password) }, }); // Login -- WRONG: "Invalid password" (reveals email exists) | CORRECT: generic message // Always run bcrypt, even for unknown emails, or response time reveals which emails exist const user = await db.user.findUnique({ where: { email } }); const hash = user?.password ?? DUMMY_HASH; // bcrypt hash of a random string, created at startup if (!(await verifyPassword(req.body.password, hash)) || !user) { return res.status(401).json({ error: "Invalid email or password" }); } ``` ### Password Policies ```typescript function validatePassword(password: string): string[] { const errors: string[] = []; if (password.length < 12) errors.push("Minimum 12 characters"); if (Buffer.byteLength(password) > 72) errors.push("Maximum 72 bytes"); // bcrypt ignores the rest // Check against breached password lists (haveibeenpwned API or local) // Do NOT enforce arbitrary complexity rules (uppercase + number + symbol) // NIST 800-63B recommends length over complexity return errors; } ``` ## Secrets Management ```python # WRONG: Hardcoded values in source code # API_KEY = "some-value-here" # CORRECT: Environment variables loaded from .env from dotenv import load_dotenv import os load_dotenv() api_key = os.getenv("API_KEY") db_url = os.getenv("DATABASE_URL") # CORRECT: Secrets manager for production # AWS: Secrets Manager, Parameter Store # GCP: Secret Manager # HashiCorp Vault for self-hosted ``` ### Secret Rotation ``` 1. Generate new secret value 2. Deploy code that accepts BOTH old and new values 3. Update all consumers to use the new value 4. Verify old value is no longer in use 5. Revoke old value Never: Rotate in-place without a transition period ``` ## Encryption Patterns ### In Transit ```typescript // Redirect HTTP to HTTPS in production app.use((req, res, next) => { if ( req.headers["x-forwarded-proto"] !== "https" && process.env.NODE_ENV === "production" ) { return res.redirect(301, `https://${req.hostname}${req.url}`); } next(); }); // HSTS header app.use((req, res, next) => { res.setHeader( "Strict-Transport-Security", "max-age=31536000; includeSubDomains", ); next(); }); ``` ### At Rest ```typescript import crypto from "crypto"; const ALGORITHM = "aes-256-gcm"; function encrypt( plaintext: string, key: Buffer, ): { ciphertext: string; iv: string; tag: string } { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let ciphertext = cipher.update(plaintext, "utf8", "hex") + cipher.final("hex"); return { ciphertext, iv: iv.toString("hex"), tag: cipher.getAuthTag().toString("hex"), }; } function decrypt( ciphertext: string, key: Buffer, iv: string, tag: string, ): string { const decipher = crypto.createDecipheriv( ALGORITHM, key, Buffer.from(iv, "hex"), ); decipher.setAuthTag(Buffer.from(tag, "hex")); return decipher.update(ciphertext, "hex", "utf8") + decipher.final("utf8"); } // Use for PII, sensitive data. Encryption key in secrets manager, NOT in code. ``` ## CORS Configuration ```typescript import cors from "cors"; // WRONG: Allow everything app.use(cors()); // origin: *, credentials: false // WRONG: Wildcard with credentials app.use(cors({ origin: "*", credentials: true })); // browsers reject this // CORRECT: Explicit allowed origins const ALLOWED_ORIGINS = [ "https://myapp.com", "https://admin.myapp.com", ...(process.env.NODE_ENV !== "production" ? ["http://localhost:3000"] : []), ]; app.use( cors({ origin: (origin, callback) => { if (!origin || ALLOWED_ORIGINS.includes(origin)) { callback(null, true); } else { callback(new Error("Not allowed by CORS")); } }, credentials: true, methods: ["GET", "POST", "PUT", "PATCH", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], maxAge: 86400, // cache preflight for 24 hours }), ); ``` ## Rate Limiting ```typescript import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import RedisStore from "rate-limit-redis"; // Global rate limit app.use( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes limit: 100, // 100 requests per window standardHeaders: true, // RateLimit-* headers
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기