security-hardening
npx skills add https://github.com/yusuketsunoda/ppt-trans --skill security-hardening
Agent 安装分布
Skill 文档
/security-hardening – Threat -> Mitigation -> Tests -> Gate
Goal
åä¸ã®è å¨ã·ããªãªãå ¥åã¨ãã¦åãåãã (1) è å¨ã¢ãã«å â (2) ç·©åè¨è¨ â (3) å®è£ æ¹é â (4) ãã¹ã/ãã° â (5) release-gateå ã¾ã§ãä¸è²«ãã¦åºããç¾ç§äºå ¸ã¯ä½ããªãã
Input (required)
| ãã£ã¼ã«ã | 説æ | ä¾ |
|---|---|---|
target |
対象(endpoint/function/module) | src/app/api/files/[id]/download/route.ts |
threat |
åç¾å¯è½ãªè å¨ã·ããªãª(1æ) | “ä»ã¦ã¼ã¶ã¼ã®fileIdãæå®ããã¨ãã¡ã¤ã«ããã¦ã³ãã¼ãã§ãã” |
environment |
ã©ã³ã¿ã¤ã | Next.js 16 + Supabase |
constraints |
äºææ§/æ§è½/éç¨å¶ç´(ããã°) | “admin-dashboardãããå¼ã¶” |
å ¥åã¯ã1ã¤ã®è å¨ãã«çµãã”IDORå ¨è¬” ã®ãããªææ§ãªå ¥åã¯ç¦æ¢ã
Output (must deliver all 5)
åºããªããªãã¹ãã«å¤±æã
| # | ææç© | å 容 |
|---|---|---|
| 1 | Threat Model | æ»æè /å½±é¿/æªç¨é£æåº¦/æ¢åé²å¾¡å±¤ |
| 2 | Mitigation Design | fail-closed/æå°æ¨©é/å¢çãã§ãã¯ã®æ¹é |
| 3 | Implementation Plan | diff-oriented: ã©ã®ãã¡ã¤ã«ã®ã©ããã©ãå¤ããã |
| 4 | Tests + Logging | åç¾ãã¹ã(ä¿®æ£åfail, ä¿®æ£å¾pass) + SecurityMonitorãã° |
| 5 | Release Gate | åçºé²æ¢ã²ã¼ãã®ææ¡(èªåæ¤åºå¯è½ãªãã®) |
Workflow
Step 0: Evidence (åç¾ç¢ºèª)
æ»æãå®éã«æç«ããããã³ã¼ããèªãã§ç¢ºèªããã
# 1. 対象ãã¡ã¤ã«ãèªã
Read <target>
# 2. èªè¨¼/èªå¯ãã§ãã¯ã®æç¡ã確èª
Grep "performSecurityChecks\|getRequestScopedAuth\|auth\.uid()" <target>
# 3. RLS policyã®ç¢ºèª(DBé¢é£ã®å ´å)
Grep "CREATE POLICY\|ALTER POLICY" supabase/migrations/ --glob "*.sql"
åºå: æ»æãæç«ããæ ¹æ (ã³ã¼ãå¼ç¨ä»ã) + æå¾ ããå¤±ææ¡ä»¶(403/401/400)
Step 1: Threat Model
以ä¸ã®ãã³ãã¬ã¼ããåãã:
### Threat Model
- **æ»æè
**: anonymous / authenticated / insider
- **å½±é¿**: data exfiltration / privilege escalation / DoS / billing fraud
- **æªç¨é£æåº¦**: low(ãã¼ã«ä¸è¦) / medium(ã«ã¹ã¿ã ãªã¯ã¨ã¹ã) / high(ç¹æ®æ¡ä»¶)
- **æ¢åã®é²å¾¡å±¤**: [RLS / JWT / performSecurityChecks / rate-limit / CSP]
- **æ¬ è½ãã¦ããé²å¾¡**: [å
·ä½çã«ä½ãè¶³ããªãã]
Step 2: Mitigation Design
åå(ãã®é ã«é©ç¨):
- Fail-closed: ã¨ã©ã¼æã¯ã¢ã¯ã»ã¹æå¦(fail-openç¦æ¢)
- Least privilege: identityã¯ãã¼ã¯ã³ããå°åºãå ¥åããåããªã
- Input validation: schema + bounds + deny-list
- Defense in depth: RLS + ã¢ããªå±¤ã®äºéãã§ãã¯
- Observability: SecurityMonitor.logEvent(PIIãªã)
Step 3: Implementation Plan (ãã¿ã¼ã³åç §)
対象ã®è å¨ã«æãè¿ããã¿ã¼ã³ãé¸ã³ãdiffæ¹éãæ¸ãã
Pattern A: IDOR (èªå¯ãã¤ãã¹)
// â BEFORE: userIdãå
¥åããåå¾
const { userId } = await request.json();
const { data } = await supabase.from("files").select().eq("user_id", userId);
// â
AFTER: auth.uid()ã«åºå® + RLSäºéé²å¾¡
const auth = await getRequestScopedAuth(request);
if (!auth.userId) return createErrorResponse("èªè¨¼ãå¿
è¦ã§ã", 401);
const { data } = await supabase.from("files").select().eq("user_id", auth.userId);
RLSå´: USING (user_id = auth.uid())
Pattern B: Token Replay (JTIãªãã¬ã¤)
// jti-tracker.ts ã®ãã¿ã¼ã³
const result = await isJtiUsed(jti);
if (result.used && !result.withinGrace) {
await invalidateUserSessions(userId); // å
¨ã»ãã·ã§ã³ç¡å¹å
return; // fail-closed
}
await markJtiAsUsed(jti, userId, expiresAt);
grace window: 並è¡ãªã¯ã¨ã¹ã許容(60ç§ããã©ã«ã)
Pattern C: Input Injection (ãã¹/URL/SQL)
// Path traversalé²å¾¡
const allowedDir = path.resolve(process.cwd(), "uploads");
const resolvedPath = path.resolve(allowedDir, filePath);
if (!resolvedPath.startsWith(allowedDir + path.sep)) {
// reject
}
// Open redirecté²å¾¡
import { sanitizeNextPath } from "@/lib/security/url-sanitizer";
const safePath = sanitizeNextPath(next, appUrl);
// null byte, backslash, //, å¤é¨origin â å
¨ã¦DEFAULT_PATHã«ãã©ã¼ã«ããã¯
Pattern D: Rate Limiting (æ°è¦ã¨ã³ããã¤ã³ã)
// performSecurityChecks ã« rateLimit ãæ¸¡ã
const securityResult = await performSecurityChecks(request, {
rateLimit: { windowMs: 3600_000, max: 50, keyPrefix: "translate" },
csrf: true,
origin: true,
});
if (!securityResult.success) {
return createErrorResponse(securityResult.error!, securityResult.status!, ...);
}
E2E対å¿: DISABLE_RATE_LIMIT_IN_E2E=true + X-Bypass-Rate-Limit ãããã¼
Pattern E: Webhook Verification
// Stripe webhook: ç½²åæ¤è¨¼ã¯ fail-closed
const sig = request.headers.get("stripe-signature");
if (!sig) return createErrorResponse("Missing signature", 400);
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
// 失æ â 400, æå â å¦çç¶è¡
Step 4: Tests + Logging
ãã¹ãæ§é : ä¿®æ£åã«æ»æãæåãããã¨ãç¢ºèª â ä¿®æ£å¾ã«å¤±æãããã¨ã確èª
// ãã¹ããã³ãã¬ã¼ã
describe("Security: [è
å¨å]", () => {
// æ£å¸¸ç³»: æ£å½ãªãªã¯ã¨ã¹ãã¯éã
it("allows legitimate request", async () => {
const response = await handler(validRequest);
expect(response.status).toBe(200);
});
// æ»æç³»: è
å¨ã·ããªãªãæå¦ããã
it("rejects [threat scenario]", async () => {
const response = await handler(maliciousRequest);
expect(response.status).toBe(403); // or 401, 400, 429
});
// ãã°ç³»: SecurityMonitorã«ã¤ãã³ããè¨é²ããã
it("logs security event", async () => {
await handler(maliciousRequest);
expect(mockLogEvent).toHaveBeenCalledWith(
expect.objectContaining({
type: expect.any(String),
severity: expect.stringMatching(/high|critical/),
})
);
});
});
Loggingå¿ é ãã§ãã¯:
SecurityMonitor.logEvent()ãè å¨ãã¹ã§å¼ã°ããã- ãã°ã«PII(email, tokenç)ãå«ã¾ããªãã
requestIdããã°ã«å«ã¾ããã(追跡ç¨)
Step 5: Release Gate (mandatory)
ä¿®æ£ã ãã§ã¯åçºãããèªåæ¤åºå¯è½ãªã²ã¼ãã1ã¤ææ¡ããã
| ã²ã¼ãç¨®å¥ | æ¤åºæ¹æ³ | ä¾ |
|---|---|---|
| API Routeå¼ã³åºãæ¼ã | grep -rL "performSecurityChecks" src/app/api/ |
å ¨API Routeã§çµ±åã»ãã¥ãªãã£ãã§ãã¯å¿ é |
| RLS policyä¸å¨ | SQLãã¤ã°ã¬ã¼ã·ã§ã³ã§CREATE TABLEãããã®ã«CREATE POLICYããªã | æ°ãã¼ãã«ã«ã¯RLSå¿ é |
| SecurityMonitoræªå°å ¥ | è å¨ãã³ããªã³ã°ã³ã¼ãã§logEventããªã | è å¨ãã¹ã«ã¯ãã°å¿ é |
| èªè¨¼ãã§ãã¯æ¼ã | getRequestScopedAuth ãå¼ã°ãã«userIdãä½¿ç¨ |
Server Actionsã§èªè¨¼å¿ é |
# ã²ã¼ãæ¤è¨¼ã³ãã³ãä¾
# 1. performSecurityChecks å¼ã³åºãæ¼ã
grep -rL "performSecurityChecks" src/app/api/*/route.ts \
| grep -v "webhook\|health\|__test"
# 2. RLSãªããã¼ãã«æ¤åº
grep -l "CREATE TABLE" supabase/migrations/*.sql | while read f; do
table=$(grep -oP 'CREATE TABLE (?:IF NOT EXISTS )?\K\w+' "$f")
if ! grep -q "CREATE POLICY.*ON $table" "$f"; then
echo "MISSING RLS: $table in $f"
fi
done
Allowlist (ä¾å¤ç®¡ç)
ä¾å¤ã¯å¿ ãããã«æè¨ãããæé»ã®ä¾å¤ã¯ç¦æ¢ã
| 対象 | ä¾å¤å 容 | çç± |
|---|---|---|
/api/stripe/webhook |
CSRFæ¤è¨¼ã¹ããã | å¤é¨Webhookã¯èªåç½²åæ¤è¨¼ |
/api/health |
èªè¨¼ã¹ããã | ãã«ã¹ãã§ãã¯(æ©å¯æ å ±ãªã) |
/api/test/* |
E2Eç°å¢ã§ã®ã¿ã¢ã¯ã»ã¹å¯ | validateE2eAccess() ã§ä¿è· |
| JTI tracker | fail-open (DBã¨ã©ã¼æ) | å¯ç¨æ§åªå (ãã°ã¯è¨é²) |
ä¾å¤ã追å ããå ´å: ãã®ãã¼ãã«ã«è¡ã追å ããPRã§æç¤ºçã«ã¬ãã¥ã¼ãåããã
Non-Goals
- è¤æ°è å¨ã1åã§å¦çããªã(1è å¨1å®è¡)
- ãã¿ã¼ã³è¾å ¸ãå¢ããã ãã®ä½æ¥ã¯ããªã(å¿ ããã¹ã+ã²ã¼ããä¼´ã)
- æ¢åã®å®å ¨ãªã³ã¼ããã念ã®ãããã§ä¿®æ£ããªã
AI Assistant Instructions
ãã®ã¹ãã«ãæå¹åãããæ:
MUST
- Step 0 (Evidence) ãå¿ ãæåã«å®è¡ãåç¾ã§ããªãè å¨ã¯æ±ããªã
- 5ã¤ã®ææç©ãã¹ã¦ãåºåããã1ã¤ã§ãæ¬ ãããã¹ãã«å¤±æ
- ãã¹ãã¯ãä¿®æ£åfail â ä¿®æ£å¾passãã®æ§é ã«ãã
- Release gateã¯grepãASTã§èªåæ¤åºå¯è½ãªãã®ã«éå®ãã
- Allowlistã«è¼ã£ã¦ããªãä¾å¤ãä½ãå ´åã¯ãã¦ã¼ã¶ã¼ã«ç¢ºèªãã
NEVER
- ãã»ãã¥ãªãã£å ¨è¬ãå¼·åãã®ãããªææ§ãªã´ã¼ã«ã§åããªã
- SecurityMonitor.logEventã«PII(email, token, password)ãæ¸¡ããªã
- fail-openãæé»ã«å°å ¥ããªã(ãããå¾ãªãå ´åã¯Allowlistã«æè¨)
- ãã¹ããªãã§ç·©åçãå®è£ ããªã