omgkit 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,57 +1,918 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: owasp
|
|
3
|
-
description: OWASP security
|
|
3
|
+
description: OWASP security best practices for web applications with vulnerability prevention, secure coding, and security testing
|
|
4
|
+
category: security
|
|
5
|
+
triggers:
|
|
6
|
+
- owasp
|
|
7
|
+
- web security
|
|
8
|
+
- security best practices
|
|
9
|
+
- vulnerability prevention
|
|
10
|
+
- secure coding
|
|
11
|
+
- penetration testing
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
# OWASP
|
|
14
|
+
# OWASP
|
|
7
15
|
|
|
8
|
-
|
|
16
|
+
Enterprise-grade **web application security** following OWASP best practices. This skill covers the OWASP Top 10 vulnerabilities, secure coding patterns, input validation, authentication security, and security testing patterns used by top engineering teams.
|
|
17
|
+
|
|
18
|
+
## Purpose
|
|
19
|
+
|
|
20
|
+
Build secure web applications:
|
|
21
|
+
|
|
22
|
+
- Prevent OWASP Top 10 vulnerabilities
|
|
23
|
+
- Implement secure coding practices
|
|
24
|
+
- Validate and sanitize user input
|
|
25
|
+
- Protect against injection attacks
|
|
26
|
+
- Secure authentication and sessions
|
|
27
|
+
- Configure security headers
|
|
28
|
+
- Implement security testing
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. Injection Prevention
|
|
9
33
|
|
|
10
|
-
### 1. Injection
|
|
11
34
|
```typescript
|
|
12
|
-
//
|
|
13
|
-
|
|
35
|
+
// lib/security/sql-injection.ts
|
|
36
|
+
import { PrismaClient } from "@prisma/client";
|
|
37
|
+
|
|
38
|
+
const prisma = new PrismaClient();
|
|
39
|
+
|
|
40
|
+
// BAD: SQL Injection vulnerable
|
|
41
|
+
async function unsafeQuery(userId: string) {
|
|
42
|
+
// NEVER DO THIS
|
|
43
|
+
return prisma.$queryRawUnsafe(
|
|
44
|
+
`SELECT * FROM users WHERE id = '${userId}'`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// GOOD: Parameterized queries
|
|
49
|
+
async function safeQuery(userId: string) {
|
|
50
|
+
return prisma.user.findUnique({
|
|
51
|
+
where: { id: userId },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// GOOD: Raw query with parameters
|
|
56
|
+
async function safeRawQuery(userId: string) {
|
|
57
|
+
return prisma.$queryRaw`
|
|
58
|
+
SELECT * FROM users WHERE id = ${userId}
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// lib/security/nosql-injection.ts
|
|
63
|
+
import { Filter } from "mongodb";
|
|
64
|
+
|
|
65
|
+
interface User {
|
|
66
|
+
_id: string;
|
|
67
|
+
email: string;
|
|
68
|
+
password: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// BAD: NoSQL Injection vulnerable
|
|
72
|
+
async function unsafeMongoQuery(db: Db, email: unknown) {
|
|
73
|
+
// If email is { $gt: "" }, this returns all users
|
|
74
|
+
return db.collection("users").findOne({ email });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// GOOD: Type validation before query
|
|
78
|
+
async function safeMongoQuery(db: Db, email: unknown) {
|
|
79
|
+
if (typeof email !== "string") {
|
|
80
|
+
throw new Error("Invalid email format");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const filter: Filter<User> = { email };
|
|
84
|
+
return db.collection<User>("users").findOne(filter);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// lib/security/command-injection.ts
|
|
88
|
+
import { execFile } from "child_process";
|
|
89
|
+
import { promisify } from "util";
|
|
90
|
+
|
|
91
|
+
const execFileAsync = promisify(execFile);
|
|
92
|
+
|
|
93
|
+
// BAD: Command Injection vulnerable
|
|
94
|
+
async function unsafeExec(filename: string) {
|
|
95
|
+
const { exec } = await import("child_process");
|
|
96
|
+
// NEVER DO THIS
|
|
97
|
+
exec(`ls -la ${filename}`, (error, stdout) => {
|
|
98
|
+
console.log(stdout);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// GOOD: Use execFile with arguments array
|
|
103
|
+
async function safeExec(filename: string) {
|
|
104
|
+
// Validate filename
|
|
105
|
+
if (!/^[\w\-. ]+$/.test(filename)) {
|
|
106
|
+
throw new Error("Invalid filename");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { stdout } = await execFileAsync("ls", ["-la", filename]);
|
|
110
|
+
return stdout;
|
|
111
|
+
}
|
|
14
112
|
|
|
15
|
-
//
|
|
16
|
-
|
|
113
|
+
// GOOD: Avoid shell commands entirely
|
|
114
|
+
import fs from "fs/promises";
|
|
115
|
+
|
|
116
|
+
async function listFiles(directory: string) {
|
|
117
|
+
// Validate and resolve path
|
|
118
|
+
const resolvedPath = path.resolve(directory);
|
|
119
|
+
const basePath = path.resolve("/allowed/base/path");
|
|
120
|
+
|
|
121
|
+
if (!resolvedPath.startsWith(basePath)) {
|
|
122
|
+
throw new Error("Path traversal attempt detected");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return fs.readdir(resolvedPath, { withFileTypes: true });
|
|
126
|
+
}
|
|
17
127
|
```
|
|
18
128
|
|
|
19
|
-
### 2.
|
|
129
|
+
### 2. XSS Prevention
|
|
130
|
+
|
|
20
131
|
```typescript
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
132
|
+
// lib/security/xss.ts
|
|
133
|
+
import DOMPurify from "isomorphic-dompurify";
|
|
134
|
+
import { escape } from "html-escaper";
|
|
135
|
+
|
|
136
|
+
// Sanitize HTML content
|
|
137
|
+
export function sanitizeHtml(dirty: string): string {
|
|
138
|
+
return DOMPurify.sanitize(dirty, {
|
|
139
|
+
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "ol", "li"],
|
|
140
|
+
ALLOWED_ATTR: ["href", "target", "rel"],
|
|
141
|
+
ALLOW_DATA_ATTR: false,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Escape for text content
|
|
146
|
+
export function escapeHtml(text: string): string {
|
|
147
|
+
return escape(text);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Safe URL validation
|
|
151
|
+
export function isValidUrl(url: string): boolean {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = new URL(url);
|
|
154
|
+
return ["http:", "https:"].includes(parsed.protocol);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// React component with XSS prevention
|
|
161
|
+
import React from "react";
|
|
162
|
+
|
|
163
|
+
interface UserContentProps {
|
|
164
|
+
content: string;
|
|
165
|
+
allowHtml?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function UserContent({ content, allowHtml = false }: UserContentProps) {
|
|
169
|
+
if (allowHtml) {
|
|
170
|
+
// Sanitize before rendering
|
|
171
|
+
const sanitized = sanitizeHtml(content);
|
|
172
|
+
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Default: escape all HTML
|
|
176
|
+
return <div>{content}</div>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Express middleware for XSS protection
|
|
180
|
+
import { Request, Response, NextFunction } from "express";
|
|
181
|
+
|
|
182
|
+
export function xssProtection(
|
|
183
|
+
req: Request,
|
|
184
|
+
res: Response,
|
|
185
|
+
next: NextFunction
|
|
186
|
+
) {
|
|
187
|
+
// Sanitize request body
|
|
188
|
+
if (req.body && typeof req.body === "object") {
|
|
189
|
+
req.body = sanitizeObject(req.body);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Sanitize query parameters
|
|
193
|
+
if (req.query && typeof req.query === "object") {
|
|
194
|
+
req.query = sanitizeObject(req.query as Record<string, unknown>);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
next();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
|
|
201
|
+
const sanitized: Record<string, unknown> = {};
|
|
202
|
+
|
|
203
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
204
|
+
if (typeof value === "string") {
|
|
205
|
+
sanitized[key] = escapeHtml(value);
|
|
206
|
+
} else if (typeof value === "object" && value !== null) {
|
|
207
|
+
sanitized[key] = sanitizeObject(value as Record<string, unknown>);
|
|
208
|
+
} else {
|
|
209
|
+
sanitized[key] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return sanitized;
|
|
214
|
+
}
|
|
24
215
|
```
|
|
25
216
|
|
|
26
|
-
### 3.
|
|
217
|
+
### 3. CSRF Protection
|
|
218
|
+
|
|
27
219
|
```typescript
|
|
28
|
-
//
|
|
29
|
-
|
|
220
|
+
// lib/security/csrf.ts
|
|
221
|
+
import crypto from "crypto";
|
|
222
|
+
import { Request, Response, NextFunction } from "express";
|
|
30
223
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
224
|
+
const CSRF_TOKEN_LENGTH = 32;
|
|
225
|
+
const CSRF_HEADER = "x-csrf-token";
|
|
226
|
+
const CSRF_COOKIE = "csrf_token";
|
|
227
|
+
|
|
228
|
+
// Generate CSRF token
|
|
229
|
+
export function generateCsrfToken(): string {
|
|
230
|
+
return crypto.randomBytes(CSRF_TOKEN_LENGTH).toString("hex");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// CSRF middleware
|
|
234
|
+
export function csrfProtection() {
|
|
235
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
236
|
+
// Skip for safe methods
|
|
237
|
+
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
|
|
238
|
+
return next();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const cookieToken = req.cookies[CSRF_COOKIE];
|
|
242
|
+
const headerToken = req.headers[CSRF_HEADER];
|
|
243
|
+
|
|
244
|
+
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
245
|
+
return res.status(403).json({
|
|
246
|
+
error: "CSRF validation failed",
|
|
247
|
+
message: "Invalid or missing CSRF token",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
next();
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Set CSRF token on response
|
|
256
|
+
export function setCsrfToken(req: Request, res: Response, next: NextFunction) {
|
|
257
|
+
if (!req.cookies[CSRF_COOKIE]) {
|
|
258
|
+
const token = generateCsrfToken();
|
|
259
|
+
res.cookie(CSRF_COOKIE, token, {
|
|
260
|
+
httpOnly: false, // Must be readable by JavaScript
|
|
261
|
+
secure: process.env.NODE_ENV === "production",
|
|
262
|
+
sameSite: "strict",
|
|
263
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
next();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// React hook for CSRF
|
|
270
|
+
export function useCsrf() {
|
|
271
|
+
const getToken = (): string | null => {
|
|
272
|
+
const match = document.cookie.match(new RegExp(`${CSRF_COOKIE}=([^;]+)`));
|
|
273
|
+
return match ? match[1] : null;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const fetchWithCsrf = async (url: string, options: RequestInit = {}) => {
|
|
277
|
+
const token = getToken();
|
|
278
|
+
|
|
279
|
+
return fetch(url, {
|
|
280
|
+
...options,
|
|
281
|
+
headers: {
|
|
282
|
+
...options.headers,
|
|
283
|
+
[CSRF_HEADER]: token || "",
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return { getToken, fetchWithCsrf };
|
|
289
|
+
}
|
|
34
290
|
```
|
|
35
291
|
|
|
36
|
-
### 4.
|
|
292
|
+
### 4. Authentication Security
|
|
293
|
+
|
|
37
294
|
```typescript
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
295
|
+
// lib/security/password.ts
|
|
296
|
+
import bcrypt from "bcrypt";
|
|
297
|
+
import crypto from "crypto";
|
|
298
|
+
|
|
299
|
+
const SALT_ROUNDS = 12;
|
|
300
|
+
const MIN_PASSWORD_LENGTH = 12;
|
|
301
|
+
const MAX_PASSWORD_LENGTH = 128;
|
|
302
|
+
|
|
303
|
+
export interface PasswordValidationResult {
|
|
304
|
+
valid: boolean;
|
|
305
|
+
errors: string[];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Password validation
|
|
309
|
+
export function validatePassword(password: string): PasswordValidationResult {
|
|
310
|
+
const errors: string[] = [];
|
|
311
|
+
|
|
312
|
+
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
313
|
+
errors.push(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (password.length > MAX_PASSWORD_LENGTH) {
|
|
317
|
+
errors.push(`Password must be at most ${MAX_PASSWORD_LENGTH} characters`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!/[a-z]/.test(password)) {
|
|
321
|
+
errors.push("Password must contain at least one lowercase letter");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!/[A-Z]/.test(password)) {
|
|
325
|
+
errors.push("Password must contain at least one uppercase letter");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!/\d/.test(password)) {
|
|
329
|
+
errors.push("Password must contain at least one digit");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
|
|
333
|
+
errors.push("Password must contain at least one special character");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check for common passwords
|
|
337
|
+
if (isCommonPassword(password)) {
|
|
338
|
+
errors.push("Password is too common");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return { valid: errors.length === 0, errors };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Hash password
|
|
345
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
346
|
+
return bcrypt.hash(password, SALT_ROUNDS);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Verify password
|
|
350
|
+
export async function verifyPassword(
|
|
351
|
+
password: string,
|
|
352
|
+
hash: string
|
|
353
|
+
): Promise<boolean> {
|
|
354
|
+
return bcrypt.compare(password, hash);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Generate secure token
|
|
358
|
+
export function generateSecureToken(length = 32): string {
|
|
359
|
+
return crypto.randomBytes(length).toString("hex");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// lib/security/rate-limit.ts
|
|
363
|
+
import rateLimit from "express-rate-limit";
|
|
364
|
+
import RedisStore from "rate-limit-redis";
|
|
365
|
+
import Redis from "ioredis";
|
|
366
|
+
|
|
367
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
368
|
+
|
|
369
|
+
// Login rate limiting
|
|
370
|
+
export const loginRateLimiter = rateLimit({
|
|
371
|
+
store: new RedisStore({
|
|
372
|
+
sendCommand: (...args: string[]) => redis.call(...args),
|
|
373
|
+
}),
|
|
374
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
375
|
+
max: 5, // 5 attempts per window
|
|
376
|
+
message: {
|
|
377
|
+
error: "Too many login attempts",
|
|
378
|
+
message: "Please try again after 15 minutes",
|
|
379
|
+
},
|
|
380
|
+
standardHeaders: true,
|
|
381
|
+
legacyHeaders: false,
|
|
382
|
+
keyGenerator: (req) => {
|
|
383
|
+
// Rate limit by IP and email combination
|
|
384
|
+
return `${req.ip}:${req.body?.email || "unknown"}`;
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// API rate limiting
|
|
389
|
+
export const apiRateLimiter = rateLimit({
|
|
390
|
+
store: new RedisStore({
|
|
391
|
+
sendCommand: (...args: string[]) => redis.call(...args),
|
|
392
|
+
}),
|
|
393
|
+
windowMs: 60 * 1000, // 1 minute
|
|
394
|
+
max: 100, // 100 requests per minute
|
|
395
|
+
message: {
|
|
396
|
+
error: "Too many requests",
|
|
397
|
+
message: "Please slow down",
|
|
398
|
+
},
|
|
399
|
+
standardHeaders: true,
|
|
400
|
+
legacyHeaders: false,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// lib/security/session.ts
|
|
404
|
+
import session from "express-session";
|
|
405
|
+
import RedisStore from "connect-redis";
|
|
406
|
+
|
|
407
|
+
export function configureSession(redis: Redis) {
|
|
408
|
+
return session({
|
|
409
|
+
store: new RedisStore({ client: redis }),
|
|
410
|
+
name: "session_id",
|
|
411
|
+
secret: process.env.SESSION_SECRET!,
|
|
412
|
+
resave: false,
|
|
413
|
+
saveUninitialized: false,
|
|
414
|
+
cookie: {
|
|
415
|
+
secure: process.env.NODE_ENV === "production",
|
|
416
|
+
httpOnly: true,
|
|
417
|
+
sameSite: "strict",
|
|
418
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
419
|
+
domain: process.env.COOKIE_DOMAIN,
|
|
420
|
+
},
|
|
421
|
+
rolling: true, // Reset expiry on activity
|
|
422
|
+
});
|
|
423
|
+
}
|
|
41
424
|
```
|
|
42
425
|
|
|
43
426
|
### 5. Security Headers
|
|
427
|
+
|
|
44
428
|
```typescript
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
429
|
+
// lib/security/headers.ts
|
|
430
|
+
import helmet from "helmet";
|
|
431
|
+
import { Express } from "express";
|
|
432
|
+
|
|
433
|
+
export function configureSecurityHeaders(app: Express) {
|
|
434
|
+
// Use helmet with custom configuration
|
|
435
|
+
app.use(
|
|
436
|
+
helmet({
|
|
437
|
+
// Content Security Policy
|
|
438
|
+
contentSecurityPolicy: {
|
|
439
|
+
directives: {
|
|
440
|
+
defaultSrc: ["'self'"],
|
|
441
|
+
scriptSrc: ["'self'", "'strict-dynamic'"],
|
|
442
|
+
styleSrc: ["'self'", "'unsafe-inline'"], // Required for many CSS-in-JS
|
|
443
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
444
|
+
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
|
445
|
+
connectSrc: ["'self'", process.env.API_URL],
|
|
446
|
+
frameSrc: ["'none'"],
|
|
447
|
+
objectSrc: ["'none'"],
|
|
448
|
+
baseUri: ["'self'"],
|
|
449
|
+
formAction: ["'self'"],
|
|
450
|
+
frameAncestors: ["'none'"],
|
|
451
|
+
upgradeInsecureRequests: [],
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// Strict Transport Security
|
|
456
|
+
strictTransportSecurity: {
|
|
457
|
+
maxAge: 31536000, // 1 year
|
|
458
|
+
includeSubDomains: true,
|
|
459
|
+
preload: true,
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
// Prevent clickjacking
|
|
463
|
+
frameguard: {
|
|
464
|
+
action: "deny",
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
// Prevent MIME sniffing
|
|
468
|
+
noSniff: true,
|
|
469
|
+
|
|
470
|
+
// XSS filter (legacy browsers)
|
|
471
|
+
xssFilter: true,
|
|
472
|
+
|
|
473
|
+
// Referrer policy
|
|
474
|
+
referrerPolicy: {
|
|
475
|
+
policy: "strict-origin-when-cross-origin",
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
// Permissions policy
|
|
479
|
+
permittedCrossDomainPolicies: {
|
|
480
|
+
permittedPolicies: "none",
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// Additional security headers
|
|
486
|
+
app.use((req, res, next) => {
|
|
487
|
+
// Prevent caching of sensitive data
|
|
488
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
489
|
+
res.setHeader("Pragma", "no-cache");
|
|
490
|
+
|
|
491
|
+
// Remove server information
|
|
492
|
+
res.removeHeader("X-Powered-By");
|
|
493
|
+
|
|
494
|
+
next();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Next.js security headers
|
|
499
|
+
// next.config.js
|
|
500
|
+
const securityHeaders = [
|
|
501
|
+
{
|
|
502
|
+
key: "X-DNS-Prefetch-Control",
|
|
503
|
+
value: "on",
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
key: "Strict-Transport-Security",
|
|
507
|
+
value: "max-age=31536000; includeSubDomains; preload",
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
key: "X-Frame-Options",
|
|
511
|
+
value: "DENY",
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
key: "X-Content-Type-Options",
|
|
515
|
+
value: "nosniff",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
key: "X-XSS-Protection",
|
|
519
|
+
value: "1; mode=block",
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
key: "Referrer-Policy",
|
|
523
|
+
value: "strict-origin-when-cross-origin",
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
key: "Permissions-Policy",
|
|
527
|
+
value: "camera=(), microphone=(), geolocation=(), interest-cohort=()",
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
key: "Content-Security-Policy",
|
|
531
|
+
value: `
|
|
532
|
+
default-src 'self';
|
|
533
|
+
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
|
534
|
+
style-src 'self' 'unsafe-inline';
|
|
535
|
+
img-src 'self' data: https:;
|
|
536
|
+
font-src 'self';
|
|
537
|
+
connect-src 'self' ${process.env.NEXT_PUBLIC_API_URL};
|
|
538
|
+
frame-ancestors 'none';
|
|
539
|
+
base-uri 'self';
|
|
540
|
+
form-action 'self';
|
|
541
|
+
`.replace(/\s+/g, " ").trim(),
|
|
542
|
+
},
|
|
543
|
+
];
|
|
544
|
+
|
|
545
|
+
module.exports = {
|
|
546
|
+
async headers() {
|
|
547
|
+
return [
|
|
548
|
+
{
|
|
549
|
+
source: "/:path*",
|
|
550
|
+
headers: securityHeaders,
|
|
551
|
+
},
|
|
552
|
+
];
|
|
553
|
+
},
|
|
554
|
+
};
|
|
50
555
|
```
|
|
51
556
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
557
|
+
### 6. Input Validation
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// lib/security/validation.ts
|
|
561
|
+
import { z } from "zod";
|
|
562
|
+
|
|
563
|
+
// Email validation schema
|
|
564
|
+
export const emailSchema = z
|
|
565
|
+
.string()
|
|
566
|
+
.email("Invalid email format")
|
|
567
|
+
.max(254, "Email too long")
|
|
568
|
+
.transform((email) => email.toLowerCase().trim());
|
|
569
|
+
|
|
570
|
+
// Password validation schema
|
|
571
|
+
export const passwordSchema = z
|
|
572
|
+
.string()
|
|
573
|
+
.min(12, "Password must be at least 12 characters")
|
|
574
|
+
.max(128, "Password must be at most 128 characters")
|
|
575
|
+
.regex(/[a-z]/, "Password must contain a lowercase letter")
|
|
576
|
+
.regex(/[A-Z]/, "Password must contain an uppercase letter")
|
|
577
|
+
.regex(/\d/, "Password must contain a digit")
|
|
578
|
+
.regex(/[!@#$%^&*]/, "Password must contain a special character");
|
|
579
|
+
|
|
580
|
+
// User registration schema
|
|
581
|
+
export const registerSchema = z.object({
|
|
582
|
+
email: emailSchema,
|
|
583
|
+
password: passwordSchema,
|
|
584
|
+
name: z
|
|
585
|
+
.string()
|
|
586
|
+
.min(2, "Name must be at least 2 characters")
|
|
587
|
+
.max(100, "Name must be at most 100 characters")
|
|
588
|
+
.regex(/^[\p{L}\s'-]+$/u, "Name contains invalid characters"),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// URL validation
|
|
592
|
+
export const urlSchema = z
|
|
593
|
+
.string()
|
|
594
|
+
.url("Invalid URL format")
|
|
595
|
+
.refine(
|
|
596
|
+
(url) => {
|
|
597
|
+
try {
|
|
598
|
+
const parsed = new URL(url);
|
|
599
|
+
return ["http:", "https:"].includes(parsed.protocol);
|
|
600
|
+
} catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
{ message: "URL must use http or https protocol" }
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// File upload validation
|
|
608
|
+
export const fileUploadSchema = z.object({
|
|
609
|
+
filename: z
|
|
610
|
+
.string()
|
|
611
|
+
.max(255)
|
|
612
|
+
.regex(
|
|
613
|
+
/^[\w\-. ]+$/,
|
|
614
|
+
"Filename contains invalid characters"
|
|
615
|
+
),
|
|
616
|
+
mimetype: z.enum([
|
|
617
|
+
"image/jpeg",
|
|
618
|
+
"image/png",
|
|
619
|
+
"image/gif",
|
|
620
|
+
"application/pdf",
|
|
621
|
+
]),
|
|
622
|
+
size: z.number().max(10 * 1024 * 1024, "File size must be under 10MB"),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Express validation middleware
|
|
626
|
+
import { Request, Response, NextFunction } from "express";
|
|
627
|
+
|
|
628
|
+
export function validate<T>(schema: z.ZodSchema<T>) {
|
|
629
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
630
|
+
try {
|
|
631
|
+
req.body = await schema.parseAsync(req.body);
|
|
632
|
+
next();
|
|
633
|
+
} catch (error) {
|
|
634
|
+
if (error instanceof z.ZodError) {
|
|
635
|
+
return res.status(400).json({
|
|
636
|
+
error: "Validation failed",
|
|
637
|
+
details: error.errors.map((e) => ({
|
|
638
|
+
field: e.path.join("."),
|
|
639
|
+
message: e.message,
|
|
640
|
+
})),
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
next(error);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Usage
|
|
649
|
+
app.post("/register", validate(registerSchema), async (req, res) => {
|
|
650
|
+
// req.body is now typed and validated
|
|
651
|
+
const { email, password, name } = req.body;
|
|
652
|
+
// ...
|
|
653
|
+
});
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### 7. Security Testing
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
// tests/security/xss.test.ts
|
|
660
|
+
import { sanitizeHtml, escapeHtml } from "@/lib/security/xss";
|
|
661
|
+
|
|
662
|
+
describe("XSS Prevention", () => {
|
|
663
|
+
describe("sanitizeHtml", () => {
|
|
664
|
+
it("removes script tags", () => {
|
|
665
|
+
const input = '<script>alert("xss")</script>';
|
|
666
|
+
expect(sanitizeHtml(input)).toBe("");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("removes event handlers", () => {
|
|
670
|
+
const input = '<img src="x" onerror="alert(1)">';
|
|
671
|
+
expect(sanitizeHtml(input)).toBe("");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("removes javascript: URLs", () => {
|
|
675
|
+
const input = '<a href="javascript:alert(1)">click</a>';
|
|
676
|
+
const result = sanitizeHtml(input);
|
|
677
|
+
expect(result).not.toContain("javascript:");
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("allows safe HTML tags", () => {
|
|
681
|
+
const input = "<p><strong>Bold</strong> and <em>italic</em></p>";
|
|
682
|
+
expect(sanitizeHtml(input)).toBe(input);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("removes data attributes", () => {
|
|
686
|
+
const input = '<div data-dangerous="value">content</div>';
|
|
687
|
+
expect(sanitizeHtml(input)).not.toContain("data-dangerous");
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
describe("escapeHtml", () => {
|
|
692
|
+
it("escapes HTML entities", () => {
|
|
693
|
+
const input = '<script>alert("xss")</script>';
|
|
694
|
+
const result = escapeHtml(input);
|
|
695
|
+
expect(result).toBe("<script>alert("xss")</script>");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("escapes ampersands", () => {
|
|
699
|
+
expect(escapeHtml("&")).toBe("&");
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// tests/security/injection.test.ts
|
|
705
|
+
import { safeQuery, safeRawQuery } from "@/lib/security/sql-injection";
|
|
706
|
+
|
|
707
|
+
describe("SQL Injection Prevention", () => {
|
|
708
|
+
it("handles malicious input safely", async () => {
|
|
709
|
+
const maliciousInput = "'; DROP TABLE users; --";
|
|
710
|
+
|
|
711
|
+
// Should not throw and should not execute injection
|
|
712
|
+
await expect(safeQuery(maliciousInput)).resolves.toBeNull();
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("uses parameterized queries", async () => {
|
|
716
|
+
const userId = "123";
|
|
717
|
+
const result = await safeRawQuery(userId);
|
|
718
|
+
|
|
719
|
+
// Query should be parameterized, not interpolated
|
|
720
|
+
expect(result).toBeDefined();
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// tests/security/csrf.test.ts
|
|
725
|
+
import request from "supertest";
|
|
726
|
+
import app from "@/app";
|
|
727
|
+
|
|
728
|
+
describe("CSRF Protection", () => {
|
|
729
|
+
it("rejects requests without CSRF token", async () => {
|
|
730
|
+
const response = await request(app)
|
|
731
|
+
.post("/api/user/profile")
|
|
732
|
+
.send({ name: "Test" });
|
|
733
|
+
|
|
734
|
+
expect(response.status).toBe(403);
|
|
735
|
+
expect(response.body.error).toBe("CSRF validation failed");
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("accepts requests with valid CSRF token", async () => {
|
|
739
|
+
// Get CSRF token
|
|
740
|
+
const getResponse = await request(app).get("/api/csrf-token");
|
|
741
|
+
const csrfToken = getResponse.body.token;
|
|
742
|
+
|
|
743
|
+
const response = await request(app)
|
|
744
|
+
.post("/api/user/profile")
|
|
745
|
+
.set("x-csrf-token", csrfToken)
|
|
746
|
+
.set("Cookie", getResponse.headers["set-cookie"])
|
|
747
|
+
.send({ name: "Test" });
|
|
748
|
+
|
|
749
|
+
expect(response.status).not.toBe(403);
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// tests/security/auth.test.ts
|
|
754
|
+
import { validatePassword, hashPassword, verifyPassword } from "@/lib/security/password";
|
|
755
|
+
|
|
756
|
+
describe("Authentication Security", () => {
|
|
757
|
+
describe("Password Validation", () => {
|
|
758
|
+
it("rejects short passwords", () => {
|
|
759
|
+
const result = validatePassword("Short1!");
|
|
760
|
+
expect(result.valid).toBe(false);
|
|
761
|
+
expect(result.errors).toContain("Password must be at least 12 characters");
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("requires complexity", () => {
|
|
765
|
+
const result = validatePassword("simplelongpassword");
|
|
766
|
+
expect(result.valid).toBe(false);
|
|
767
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it("accepts strong passwords", () => {
|
|
771
|
+
const result = validatePassword("SecureP@ssw0rd123!");
|
|
772
|
+
expect(result.valid).toBe(true);
|
|
773
|
+
expect(result.errors).toHaveLength(0);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
describe("Password Hashing", () => {
|
|
778
|
+
it("hashes passwords securely", async () => {
|
|
779
|
+
const password = "SecureP@ssw0rd123!";
|
|
780
|
+
const hash = await hashPassword(password);
|
|
781
|
+
|
|
782
|
+
expect(hash).not.toBe(password);
|
|
783
|
+
expect(hash.startsWith("$2b$")).toBe(true);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it("verifies correct passwords", async () => {
|
|
787
|
+
const password = "SecureP@ssw0rd123!";
|
|
788
|
+
const hash = await hashPassword(password);
|
|
789
|
+
|
|
790
|
+
expect(await verifyPassword(password, hash)).toBe(true);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("rejects incorrect passwords", async () => {
|
|
794
|
+
const hash = await hashPassword("SecureP@ssw0rd123!");
|
|
795
|
+
|
|
796
|
+
expect(await verifyPassword("WrongPassword1!", hash)).toBe(false);
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
## Use Cases
|
|
803
|
+
|
|
804
|
+
### Security Audit Checklist
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
// lib/security/audit.ts
|
|
808
|
+
export interface SecurityAuditResult {
|
|
809
|
+
category: string;
|
|
810
|
+
check: string;
|
|
811
|
+
status: "pass" | "fail" | "warning";
|
|
812
|
+
message: string;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export async function runSecurityAudit(): Promise<SecurityAuditResult[]> {
|
|
816
|
+
const results: SecurityAuditResult[] = [];
|
|
817
|
+
|
|
818
|
+
// Check HTTPS
|
|
819
|
+
results.push({
|
|
820
|
+
category: "Transport",
|
|
821
|
+
check: "HTTPS Enabled",
|
|
822
|
+
status: process.env.NODE_ENV === "production" ? "pass" : "warning",
|
|
823
|
+
message: "HTTPS should be enabled in production",
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Check security headers
|
|
827
|
+
results.push({
|
|
828
|
+
category: "Headers",
|
|
829
|
+
check: "Security Headers",
|
|
830
|
+
status: "pass",
|
|
831
|
+
message: "Helmet middleware configured",
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Check rate limiting
|
|
835
|
+
results.push({
|
|
836
|
+
category: "Rate Limiting",
|
|
837
|
+
check: "Login Rate Limiting",
|
|
838
|
+
status: "pass",
|
|
839
|
+
message: "Login endpoints rate limited",
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// Check CSRF protection
|
|
843
|
+
results.push({
|
|
844
|
+
category: "CSRF",
|
|
845
|
+
check: "CSRF Protection",
|
|
846
|
+
status: "pass",
|
|
847
|
+
message: "CSRF tokens required for state-changing requests",
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
return results;
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Dependency Scanning
|
|
855
|
+
|
|
856
|
+
```yaml
|
|
857
|
+
# .github/workflows/security.yml
|
|
858
|
+
name: Security Scan
|
|
859
|
+
|
|
860
|
+
on:
|
|
861
|
+
push:
|
|
862
|
+
branches: [main]
|
|
863
|
+
schedule:
|
|
864
|
+
- cron: "0 0 * * *" # Daily
|
|
865
|
+
|
|
866
|
+
jobs:
|
|
867
|
+
security:
|
|
868
|
+
runs-on: ubuntu-latest
|
|
869
|
+
steps:
|
|
870
|
+
- uses: actions/checkout@v4
|
|
871
|
+
|
|
872
|
+
- name: Run npm audit
|
|
873
|
+
run: npm audit --audit-level=high
|
|
874
|
+
|
|
875
|
+
- name: Run Snyk scan
|
|
876
|
+
uses: snyk/actions/node@master
|
|
877
|
+
env:
|
|
878
|
+
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
|
879
|
+
|
|
880
|
+
- name: Run SAST scan
|
|
881
|
+
uses: github/codeql-action/analyze@v3
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
## Best Practices
|
|
885
|
+
|
|
886
|
+
### Do's
|
|
887
|
+
|
|
888
|
+
- Validate all user input on the server
|
|
889
|
+
- Use parameterized queries for database access
|
|
890
|
+
- Implement proper authentication and session management
|
|
891
|
+
- Set security headers on all responses
|
|
892
|
+
- Use HTTPS for all communications
|
|
893
|
+
- Implement rate limiting on sensitive endpoints
|
|
894
|
+
- Log security events for monitoring
|
|
895
|
+
- Keep dependencies updated
|
|
896
|
+
- Conduct regular security audits
|
|
897
|
+
- Follow the principle of least privilege
|
|
898
|
+
|
|
899
|
+
### Don'ts
|
|
900
|
+
|
|
901
|
+
- Don't trust client-side validation alone
|
|
902
|
+
- Don't store sensitive data in plain text
|
|
903
|
+
- Don't expose detailed error messages to users
|
|
904
|
+
- Don't use deprecated cryptographic algorithms
|
|
905
|
+
- Don't disable security features for convenience
|
|
906
|
+
- Don't hardcode secrets in source code
|
|
907
|
+
- Don't ignore security warnings
|
|
908
|
+
- Don't use eval() or similar functions
|
|
909
|
+
- Don't allow unlimited file uploads
|
|
910
|
+
- Don't skip security testing
|
|
911
|
+
|
|
912
|
+
## References
|
|
913
|
+
|
|
914
|
+
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
|
915
|
+
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
|
|
916
|
+
- [OWASP Testing Guide](https://owasp.org/www-project-web-security-testing-guide/)
|
|
917
|
+
- [OWASP ASVS](https://owasp.org/www-project-application-security-verification-standard/)
|
|
918
|
+
- [CWE Top 25](https://cwe.mitre.org/top25/archive/2023/2023_top25_list.html)
|