getdoorman 1.0.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/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Data-driven JS/TS auto-fixes (80 entries)
|
|
2
|
+
const fixes = [
|
|
3
|
+
// --- XSS Prevention (1-10) ---
|
|
4
|
+
{ id: 'rfx-js-001', match: /\.innerHTML\s*=\s*([^;]+)/g, replace: '.textContent = $1', lang: 'js', tier: 1, title: 'Replace innerHTML with textContent' },
|
|
5
|
+
{ id: 'rfx-js-002', match: /document\.write\(([^)]+)\)/g, replace: '/* SECURITY: document.write removed */ document.getElementById("app").textContent = $1', lang: 'js', tier: 2, title: 'Replace document.write with safe DOM update' },
|
|
6
|
+
{ id: 'rfx-js-003', match: /\$\(([^)]+)\)\.html\(([^)]+)\)/g, replace: '$($1).text($2)', lang: 'js', tier: 1, title: 'Replace jQuery .html() with .text()' },
|
|
7
|
+
{ id: 'rfx-js-004', match: /insertAdjacentHTML\(\s*['"]beforeend['"]\s*,\s*([^)]+)\)/g, replace: 'insertAdjacentText("beforeend", $1)', lang: 'js', tier: 1, title: 'Replace insertAdjacentHTML with insertAdjacentText' },
|
|
8
|
+
{ id: 'rfx-js-005', match: /outerHTML\s*=\s*([^;]+)/g, replace: '/* SECURITY: outerHTML replaced — use textContent or DOM APIs */ textContent = $1', lang: 'js', tier: 2, title: 'Replace outerHTML assignment' },
|
|
9
|
+
{ id: 'rfx-js-006', match: /\bres\.send\(\s*['"`]<[^>]+>/g, replace: (m) => m.replace('res.send(', 'res.type("text/plain").send('), lang: 'js', tier: 1, title: 'Set content-type to text/plain for HTML-like responses' },
|
|
10
|
+
{ id: 'rfx-js-007', match: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html:\s*([^}]+)\}\s*\}/g, replace: '/* SECURITY: sanitize before dangerouslySetInnerHTML */ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize($1) }}', lang: 'js', tier: 2, title: 'Wrap dangerouslySetInnerHTML with DOMPurify' },
|
|
11
|
+
{ id: 'rfx-js-008', match: /v-html\s*=\s*"([^"]+)"/g, replace: 'v-text="$1"', lang: 'js', tier: 1, title: 'Replace Vue v-html with v-text' },
|
|
12
|
+
{ id: 'rfx-js-009', match: /\[innerHTML\]\s*=\s*"([^"]+)"/g, replace: '[textContent]="$1"', lang: 'js', tier: 1, title: 'Replace Angular innerHTML binding with textContent' },
|
|
13
|
+
{ id: 'rfx-js-010', match: /new\s+Function\(\s*(['"`][^'"`]*['"`]\s*\+\s*\w+|[^'"`)\s]+\s*\+)/g, replace: (m) => `/* SECURITY: dynamic Function() is unsafe */ ${m}`, lang: 'js', tier: 2, title: 'Flag dynamic Function constructor' },
|
|
14
|
+
|
|
15
|
+
// --- Cookie/Session Security (11-20) ---
|
|
16
|
+
{ id: 'rfx-js-011', match: /res\.cookie\(([^,]+),\s*([^,]+)\)/g, replace: 'res.cookie($1, $2, { httpOnly: true, secure: true, sameSite: "strict" })', lang: 'js', tier: 1, title: 'Add security flags to res.cookie' },
|
|
17
|
+
{ id: 'rfx-js-012', match: /cookie:\s*\{([^}]*?)(?!secure)\}/g, replace: (m) => m.includes('secure') ? m : m.replace('}', ', secure: true }'), lang: 'js', tier: 1, title: 'Add secure flag to cookie config' },
|
|
18
|
+
{ id: 'rfx-js-013', match: /session\(\s*\{([^}]*?)secret:\s*['"]([^'"]{1,20})['"]/g, replace: (m) => m.replace(/secret:\s*['"][^'"]+['"]/, 'secret: process.env.SESSION_SECRET'), lang: 'js', tier: 1, title: 'Move session secret to env var' },
|
|
19
|
+
{ id: 'rfx-js-014', match: /saveUninitialized:\s*true/g, replace: 'saveUninitialized: false', lang: 'js', tier: 1, title: 'Disable saveUninitialized in session config' },
|
|
20
|
+
{ id: 'rfx-js-015', match: /resave:\s*true/g, replace: 'resave: false', lang: 'js', tier: 1, title: 'Disable resave in session config' },
|
|
21
|
+
{ id: 'rfx-js-016', match: /document\.cookie\s*=\s*([^;]+)/g, replace: '/* SECURITY: use httpOnly cookies via server */ document.cookie = $1', lang: 'js', tier: 2, title: 'Warn about client-side cookie access' },
|
|
22
|
+
{ id: 'rfx-js-017', match: /maxAge:\s*(\d{10,})/g, replace: 'maxAge: 86400000 /* 24h — was excessively long */', lang: 'js', tier: 1, title: 'Cap excessive cookie maxAge' },
|
|
23
|
+
{ id: 'rfx-js-018', match: /express-session'\)(?![^]*store:)/gm, replace: (m) => m, lang: 'js', tier: 2, title: 'Warn: session without persistent store' },
|
|
24
|
+
{ id: 'rfx-js-019', match: /httpOnly:\s*false/g, replace: 'httpOnly: true', lang: 'js', tier: 1, title: 'Enable httpOnly on cookies' },
|
|
25
|
+
{ id: 'rfx-js-020', match: /sameSite:\s*['"]?none['"]?/gi, replace: 'sameSite: "strict"', lang: 'js', tier: 1, title: 'Upgrade sameSite from none to strict' },
|
|
26
|
+
|
|
27
|
+
// --- SQL/NoSQL Injection (21-30) ---
|
|
28
|
+
{ id: 'rfx-js-021', match: /query\(\s*`SELECT\s+\*\s+FROM\s+\w+\s+WHERE\s+\w+\s*=\s*\$\{([^}]+)\}`/gi, replace: (m, val) => m.replace(`\${${val}}`, '?').replace('`', '"') + `, [${val}]`, lang: 'js', tier: 2, title: 'Parameterize SQL template literal' },
|
|
29
|
+
{ id: 'rfx-js-022', match: /query\(\s*['"]SELECT\s+\*\s+FROM\s+\w+\s+WHERE\s+\w+\s*=\s*['"]\s*\+\s*(\w+)/gi, replace: (m, val) => `query("SELECT * FROM table WHERE col = ?", [${val}]`, lang: 'js', tier: 2, title: 'Parameterize SQL string concatenation' },
|
|
30
|
+
{ id: 'rfx-js-023', match: /\$where:\s*['"`].*\+\s*\w+/g, replace: (m) => `/* SECURITY: $where with user input is NoSQL injection */ ${m}`, lang: 'js', tier: 2, title: 'Flag MongoDB $where injection' },
|
|
31
|
+
{ id: 'rfx-js-024', match: /\.find\(\s*\{[^}]*:\s*req\.(body|query|params)\.\w+\s*\}/g, replace: (m) => m.replace(/req\.(body|query|params)\.(\w+)/, 'sanitize(req.$1.$2)'), lang: 'js', tier: 2, title: 'Sanitize MongoDB query input' },
|
|
32
|
+
{ id: 'rfx-js-025', match: /\.findOne\(\s*\{[^}]*:\s*req\.(body|query|params)\.\w+\s*\}/g, replace: (m) => m.replace(/req\.(body|query|params)\.(\w+)/, 'String(req.$1.$2)'), lang: 'js', tier: 1, title: 'Cast MongoDB findOne input to string' },
|
|
33
|
+
{ id: 'rfx-js-026', match: /\.aggregate\(\s*\[\s*\{\s*\$match:\s*\{[^}]*:\s*req\./g, replace: (m) => `/* SECURITY: validate aggregation input */ ${m}`, lang: 'js', tier: 2, title: 'Flag unsanitized MongoDB aggregation' },
|
|
34
|
+
{ id: 'rfx-js-027', match: /\.updateOne\(\s*\{[^}]*:\s*req\.(body|query|params)/g, replace: (m) => `/* SECURITY: validate update filter */ ${m}`, lang: 'js', tier: 2, title: 'Flag unsanitized MongoDB update' },
|
|
35
|
+
{ id: 'rfx-js-028', match: /\.deleteOne\(\s*\{[^}]*:\s*req\.(body|query|params)/g, replace: (m) => `/* SECURITY: validate delete filter */ ${m}`, lang: 'js', tier: 2, title: 'Flag unsanitized MongoDB delete' },
|
|
36
|
+
{ id: 'rfx-js-029', match: /knex\.raw\(\s*['"`].*\$\{/g, replace: (m) => `/* SECURITY: use knex.raw("...?", [val]) instead of template */ ${m}`, lang: 'js', tier: 2, title: 'Flag knex.raw template injection' },
|
|
37
|
+
{ id: 'rfx-js-030', match: /sequelize\.query\(\s*`[^`]*\$\{/g, replace: (m) => `/* SECURITY: use bind parameters with sequelize.query */ ${m}`, lang: 'js', tier: 2, title: 'Flag Sequelize raw query injection' },
|
|
38
|
+
|
|
39
|
+
// --- Crypto/Auth (31-40) ---
|
|
40
|
+
{ id: 'rfx-js-031', match: /createHash\(['"]md5['"]\)/g, replace: 'createHash("sha256")', lang: 'js', tier: 1, title: 'Replace MD5 with SHA-256' },
|
|
41
|
+
{ id: 'rfx-js-032', match: /createHash\(['"]sha1['"]\)/g, replace: 'createHash("sha256")', lang: 'js', tier: 1, title: 'Replace SHA-1 with SHA-256' },
|
|
42
|
+
{ id: 'rfx-js-033', match: /createCipheriv\(['"]des['"]/gi, replace: 'createCipheriv("aes-256-gcm"', lang: 'js', tier: 1, title: 'Replace DES with AES-256-GCM' },
|
|
43
|
+
{ id: 'rfx-js-034', match: /createCipheriv\(['"]aes-128-ecb['"]/gi, replace: 'createCipheriv("aes-256-gcm"', lang: 'js', tier: 1, title: 'Replace AES-ECB with AES-GCM' },
|
|
44
|
+
{ id: 'rfx-js-035', match: /Math\.random\(\)\s*\.toString\(36\)/g, replace: 'crypto.randomUUID()', lang: 'js', tier: 1, title: 'Replace Math.random token with crypto.randomUUID' },
|
|
45
|
+
{ id: 'rfx-js-036', match: /jwt\.sign\(([^,]+),\s*([^,]+)\)/g, replace: 'jwt.sign($1, $2, { expiresIn: "1h", algorithm: "HS256" })', lang: 'js', tier: 1, title: 'Add expiry and algorithm to JWT sign' },
|
|
46
|
+
{ id: 'rfx-js-037', match: /bcrypt\.hashSync\(([^,]+),\s*(\d)\)/g, replace: 'bcrypt.hashSync($1, 12)', lang: 'js', tier: 1, title: 'Increase bcrypt rounds to 12' },
|
|
47
|
+
{ id: 'rfx-js-038', match: /password\s*===?\s*['"][^'"]+['"]/g, replace: '/* SECURITY: never compare passwords as plaintext — use bcrypt.compare() */', lang: 'js', tier: 2, title: 'Flag plaintext password comparison' },
|
|
48
|
+
{ id: 'rfx-js-039', match: /algorithms:\s*\[\s*['"]none['"]\s*\]/g, replace: 'algorithms: ["HS256"]', lang: 'js', tier: 1, title: 'Remove "none" from JWT algorithms' },
|
|
49
|
+
{ id: 'rfx-js-040', match: /\.generateKeyPairSync\(['"]rsa['"],\s*\{\s*modulusLength:\s*(512|1024)\b/g, replace: (m) => m.replace(/modulusLength:\s*\d+/, 'modulusLength: 2048'), lang: 'js', tier: 1, title: 'Increase RSA key length to 2048' },
|
|
50
|
+
|
|
51
|
+
// --- HTTP Security (41-50) ---
|
|
52
|
+
{ id: 'rfx-js-041', match: /cors\(\s*\)/g, replace: 'cors({ origin: process.env.ALLOWED_ORIGIN || "http://localhost:3000", credentials: true })', lang: 'js', tier: 1, title: 'Restrict CORS from wildcard' },
|
|
53
|
+
{ id: 'rfx-js-042', match: /cors\(\s*\{\s*origin:\s*['"]\*['"]/g, replace: 'cors({ origin: process.env.ALLOWED_ORIGIN', lang: 'js', tier: 1, title: 'Replace CORS wildcard origin' },
|
|
54
|
+
{ id: 'rfx-js-043', match: /Access-Control-Allow-Origin['"]\s*,\s*['"]?\*/g, replace: 'Access-Control-Allow-Origin", process.env.ALLOWED_ORIGIN', lang: 'js', tier: 1, title: 'Replace Access-Control-Allow-Origin wildcard' },
|
|
55
|
+
{ id: 'rfx-js-044', match: /app\.disable\(['"]x-powered-by['"]\)/g, replace: 'app.disable("x-powered-by") /* good — also consider helmet() */', lang: 'js', tier: 1, title: 'Confirm x-powered-by disabled' },
|
|
56
|
+
{ id: 'rfx-js-045', match: /X-Frame-Options['"]\s*,\s*['"]ALLOWALL['"]/gi, replace: 'X-Frame-Options", "DENY"', lang: 'js', tier: 1, title: 'Set X-Frame-Options to DENY' },
|
|
57
|
+
{ id: 'rfx-js-046', match: /Strict-Transport-Security['"]\s*,\s*['"]max-age=0/g, replace: 'Strict-Transport-Security", "max-age=31536000; includeSubDomains', lang: 'js', tier: 1, title: 'Set proper HSTS max-age' },
|
|
58
|
+
{ id: 'rfx-js-047', match: /http:\/\/localhost/g, replace: 'http://localhost', lang: 'js', tier: 1, title: 'Keep localhost HTTP (no fix needed)' },
|
|
59
|
+
{ id: 'rfx-js-048', match: /app\.listen\((\d+)\)/g, replace: 'app.listen(parseInt(process.env.PORT) || $1)', lang: 'js', tier: 1, title: 'Use PORT env var for server listen' },
|
|
60
|
+
{ id: 'rfx-js-049', match: /X-Content-Type-Options['"]\s*,\s*['"]false['"]/gi, replace: 'X-Content-Type-Options", "nosniff"', lang: 'js', tier: 1, title: 'Set X-Content-Type-Options to nosniff' },
|
|
61
|
+
{ id: 'rfx-js-050', match: /Referrer-Policy['"]\s*,\s*['"]unsafe-url['"]/gi, replace: 'Referrer-Policy", "strict-origin-when-cross-origin"', lang: 'js', tier: 1, title: 'Set safe Referrer-Policy' },
|
|
62
|
+
|
|
63
|
+
// --- Input Validation (51-60) ---
|
|
64
|
+
{ id: 'rfx-js-051', match: /Object\.assign\(\s*\{\},?\s*req\.body\)/g, replace: '/* SECURITY: validate req.body before spreading */ Object.assign({}, req.body)', lang: 'js', tier: 2, title: 'Flag unvalidated Object.assign from req.body' },
|
|
65
|
+
{ id: 'rfx-js-052', match: /\.\.\.(req\.body|req\.query|req\.params)/g, replace: '/* SECURITY: validate before spreading */ ...$1', lang: 'js', tier: 2, title: 'Flag spread of user input' },
|
|
66
|
+
{ id: 'rfx-js-053', match: /\[req\.(body|query|params)\.\w+\]/g, replace: (m) => `/* SECURITY: bracket notation with user input — prototype pollution risk */ ${m}`, lang: 'js', tier: 2, title: 'Flag bracket notation with user input' },
|
|
67
|
+
{ id: 'rfx-js-054', match: /path\.join\(\s*\w+\s*,\s*req\.(body|query|params)\.\w+\)/g, replace: (m) => m.replace('path.join(', 'path.join(/* SECURITY: validate path */ '), lang: 'js', tier: 2, title: 'Flag path.join with user input' },
|
|
68
|
+
{ id: 'rfx-js-055', match: /new\s+RegExp\(\s*req\.(body|query|params)/g, replace: (m) => `/* SECURITY: ReDoS risk — escape user input */ ${m}`, lang: 'js', tier: 2, title: 'Flag RegExp with user input (ReDoS)' },
|
|
69
|
+
{ id: 'rfx-js-056', match: /parseInt\(\s*req\.(body|query|params)\.(\w+)\s*\)/g, replace: 'Number.parseInt(req.$1.$2, 10)', lang: 'js', tier: 1, title: 'Add radix to parseInt' },
|
|
70
|
+
{ id: 'rfx-js-057', match: /JSON\.parse\(\s*req\.(body|query|params)/g, replace: (m) => `/* SECURITY: wrap in try-catch */ ${m}`, lang: 'js', tier: 2, title: 'Flag JSON.parse of user input without try-catch' },
|
|
71
|
+
{ id: 'rfx-js-058', match: /req\.query\.\w+\s*\|\|\s*['"]/g, replace: (m) => m, lang: 'js', tier: 1, title: 'Validate query params before use' },
|
|
72
|
+
{ id: 'rfx-js-059', match: /encodeURIComponent/g, replace: 'encodeURIComponent', lang: 'js', tier: 1, title: 'Confirm URI encoding (no fix needed)' },
|
|
73
|
+
{ id: 'rfx-js-060', match: /redirect\(\s*req\.(body|query|params)\.\w+\s*\)/g, replace: (m) => `/* SECURITY: open redirect — validate URL */ ${m}`, lang: 'js', tier: 2, title: 'Flag open redirect from user input' },
|
|
74
|
+
|
|
75
|
+
// --- Error Handling (61-70) ---
|
|
76
|
+
{ id: 'rfx-js-061', match: /res\.(send|json)\(\s*\{\s*error:\s*err\.stack/g, replace: 'res.$1({ error: "Internal server error"', lang: 'js', tier: 1, title: 'Remove stack trace from error response' },
|
|
77
|
+
{ id: 'rfx-js-062', match: /res\.(send|json)\(\s*\{\s*error:\s*err\.message/g, replace: 'res.$1({ error: process.env.NODE_ENV === "production" ? "Internal server error" : err.message', lang: 'js', tier: 1, title: 'Hide error messages in production' },
|
|
78
|
+
{ id: 'rfx-js-063', match: /catch\s*\(\s*\)\s*\{/g, replace: 'catch (err) {', lang: 'js', tier: 1, title: 'Add error parameter to catch block' },
|
|
79
|
+
{ id: 'rfx-js-064', match: /catch\s*\(\s*\w+\s*\)\s*\{\s*\}/g, replace: (m) => m.replace('{}', '{ /* TODO: handle error */ }'), lang: 'js', tier: 1, title: 'Flag empty catch block' },
|
|
80
|
+
{ id: 'rfx-js-065', match: /process\.on\(['"]uncaughtException['"],\s*\(\s*\)\s*=>\s*\{\s*\}\)/g, replace: 'process.on("uncaughtException", (err) => { console.error("Uncaught:", err); process.exit(1); })', lang: 'js', tier: 1, title: 'Handle uncaughtException properly' },
|
|
81
|
+
{ id: 'rfx-js-066', match: /process\.on\(['"]unhandledRejection['"],\s*\(\s*\)\s*=>\s*\{\s*\}\)/g, replace: 'process.on("unhandledRejection", (reason) => { console.error("Unhandled rejection:", reason); })', lang: 'js', tier: 1, title: 'Handle unhandledRejection properly' },
|
|
82
|
+
{ id: 'rfx-js-067', match: /\.then\(([^)]+)\)(?!\s*\.catch)/g, replace: '.then($1).catch(err => console.error(err))', lang: 'js', tier: 1, title: 'Add .catch to promise chain' },
|
|
83
|
+
{ id: 'rfx-js-068', match: /throw\s+['"]([^'"]+)['"]/g, replace: 'throw new Error("$1")', lang: 'js', tier: 1, title: 'Throw Error objects instead of strings' },
|
|
84
|
+
{ id: 'rfx-js-069', match: /console\.error\(err\);\s*res\.status\(500\)\.send\(err\)/g, replace: 'console.error(err); res.status(500).json({ error: "Internal server error" })', lang: 'js', tier: 1, title: 'Don\'t send raw error to client' },
|
|
85
|
+
{ id: 'rfx-js-070', match: /app\.use\(\(\s*err[^)]*\)\s*=>\s*\{[^}]*res\.send\(err\)/g, replace: (m) => m.replace('res.send(err)', 'res.status(500).json({ error: "Internal server error" })'), lang: 'js', tier: 1, title: 'Sanitize error handler response' },
|
|
86
|
+
|
|
87
|
+
// --- Misc JS (71-80) ---
|
|
88
|
+
{ id: 'rfx-js-071', match: /console\.log\(.*password.*\)/gi, replace: '/* SECURITY: removed — do not log passwords */', lang: 'js', tier: 1, title: 'Remove password logging' },
|
|
89
|
+
{ id: 'rfx-js-072', match: /console\.log\(.*token.*\)/gi, replace: '/* SECURITY: removed — do not log tokens */', lang: 'js', tier: 1, title: 'Remove token logging' },
|
|
90
|
+
{ id: 'rfx-js-073', match: /console\.log\(.*secret.*\)/gi, replace: '/* SECURITY: removed — do not log secrets */', lang: 'js', tier: 1, title: 'Remove secret logging' },
|
|
91
|
+
{ id: 'rfx-js-074', match: /:\s*any\b/g, replace: ': unknown', lang: 'js', tier: 1, title: 'Replace TypeScript any with unknown' },
|
|
92
|
+
{ id: 'rfx-js-075', match: /process\.env\.NODE_ENV\s*!==?\s*['"]production['"]\s*\?\s*true\s*:\s*false/g, replace: 'process.env.NODE_ENV !== "production"', lang: 'js', tier: 1, title: 'Simplify NODE_ENV ternary' },
|
|
93
|
+
{ id: 'rfx-js-076', match: /['"]use strict['"];?/g, replace: '"use strict";', lang: 'js', tier: 1, title: 'Normalize use strict' },
|
|
94
|
+
{ id: 'rfx-js-077', match: /setTimeout\(\s*([^,]+),\s*0\s*\)/g, replace: 'queueMicrotask($1)', lang: 'js', tier: 1, title: 'Replace setTimeout(fn,0) with queueMicrotask' },
|
|
95
|
+
{ id: 'rfx-js-078', match: /new\s+Buffer\(/g, replace: 'Buffer.from(', lang: 'js', tier: 1, title: 'Replace deprecated new Buffer with Buffer.from' },
|
|
96
|
+
{ id: 'rfx-js-079', match: /require\(['"]child_process['"]\)\.exec\(/g, replace: 'require("child_process").execFile(', lang: 'js', tier: 1, title: 'Replace exec with execFile' },
|
|
97
|
+
{ id: 'rfx-js-080', match: /fs\.chmod(?:Sync)?\([^,]+,\s*0o?777\)/g, replace: (m) => m.replace(/0o?777/, '0o755'), lang: 'js', tier: 1, title: 'Reduce file permissions from 777 to 755' },
|
|
98
|
+
];
|
|
99
|
+
export default fixes;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Data-driven MCP Server + AI API auto-fixes (50 entries: 25 MCP + 25 AI)
|
|
2
|
+
const fixes = [
|
|
3
|
+
// --- MCP Server Fixes (1-25) ---
|
|
4
|
+
{ id: 'rfx-mcp-001', match: /server\.tool\(\s*['"](\w+)['"]\s*,\s*async\s*\(\s*\{/g, replace: 'server.tool("$1", async ({ /* validate params with zod */\n', lang: 'any', tier: 2, title: 'Add param validation to MCP tool handler' },
|
|
5
|
+
{ id: 'rfx-mcp-002', match: /setRequestHandler\(\s*\w+\s*,\s*async\s*\(/g, replace: '/* add auth check */ setRequestHandler(handler, async (', lang: 'any', tier: 2, title: 'Add auth to MCP request handler' },
|
|
6
|
+
{ id: 'rfx-mcp-003', match: /fs\.readFileSync\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: sandbox file access */ fs.readFileSync(path.resolve(SANDBOX_DIR, path.basename(params.', lang: 'any', tier: 2, title: 'Sandbox MCP file system access' },
|
|
7
|
+
{ id: 'rfx-mcp-004', match: /fs\.writeFileSync\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: sandbox file access */ fs.writeFileSync(path.resolve(SANDBOX_DIR, path.basename(params.', lang: 'any', tier: 2, title: 'Sandbox MCP file write access' },
|
|
8
|
+
{ id: 'rfx-mcp-005', match: /exec\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: command injection via MCP params */ execFile(params.', lang: 'any', tier: 2, title: 'Replace exec with execFile in MCP handler' },
|
|
9
|
+
{ id: 'rfx-mcp-006', match: /query\(\s*`[^`]*\$\{(?:params|args|input)\./g, replace: (m) => `/* SECURITY: SQL injection via MCP params — use parameterized query */ ${m}`, lang: 'any', tier: 2, title: 'Flag SQL injection in MCP handler' },
|
|
10
|
+
{ id: 'rfx-mcp-007', match: /McpServer\(\s*\{[^}]*transport:\s*['"]http['"](?!s)/g, replace: (m) => m.replace('"http"', '"https"'), lang: 'any', tier: 1, title: 'Use HTTPS for MCP transport' },
|
|
11
|
+
{ id: 'rfx-mcp-008', match: /StdioServerTransport\(\)/g, replace: 'StdioServerTransport() /* consider adding request size limits */', lang: 'any', tier: 2, title: 'Note MCP transport size limits' },
|
|
12
|
+
{ id: 'rfx-mcp-009', match: /return\s*\{\s*content:\s*\[\s*\{\s*type:\s*['"]text['"],\s*text:\s*JSON\.stringify\(\s*(?:result|data|rows)/g, replace: (m) => `/* filter sensitive fields before returning */ ${m}`, lang: 'any', tier: 2, title: 'Filter sensitive data from MCP response' },
|
|
13
|
+
{ id: 'rfx-mcp-010', match: /process\.env\b/g, replace: 'process.env', lang: 'any', tier: 1, title: 'Confirm env var usage (no fix needed)' },
|
|
14
|
+
{ id: 'rfx-mcp-011', match: /tool\(\s*['"][^'"]+['"]\s*,\s*['"][^'"]*<script/gi, replace: (m) => `/* SECURITY: XSS in tool description */ ${m}`, lang: 'any', tier: 2, title: 'Flag XSS in MCP tool description' },
|
|
15
|
+
{ id: 'rfx-mcp-012', match: /description:\s*['"][^'"]*\{\{/g, replace: (m) => `/* SECURITY: template injection in description */ ${m}`, lang: 'any', tier: 2, title: 'Flag template injection in MCP description' },
|
|
16
|
+
{ id: 'rfx-mcp-013', match: /catch\s*\(\s*\w*\s*\)\s*\{\s*return\s*\{\s*content.*error/g, replace: (m) => m.replace(/error\.\w+/, '"Internal error"'), lang: 'any', tier: 1, title: 'Sanitize MCP error responses' },
|
|
17
|
+
{ id: 'rfx-mcp-014', match: /eval\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: code injection via MCP params — REMOVE eval */ JSON.parse(params.', lang: 'any', tier: 2, title: 'Remove eval in MCP handler' },
|
|
18
|
+
{ id: 'rfx-mcp-015', match: /new\s+Function\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: code injection via MCP params */ new Function(params.', lang: 'any', tier: 2, title: 'Flag Function() in MCP handler' },
|
|
19
|
+
{ id: 'rfx-mcp-016', match: /console\.log\(\s*['"]Tool called/g, replace: '/* add structured audit logging */ logger.info("Tool called', lang: 'any', tier: 1, title: 'Use structured logging in MCP' },
|
|
20
|
+
{ id: 'rfx-mcp-017', match: /console\.log\(\s*JSON\.stringify\(\s*params/g, replace: '/* SECURITY: do not log raw params */ logger.info("Tool invoked", { tool: params.name })', lang: 'any', tier: 1, title: 'Redact MCP params from logs' },
|
|
21
|
+
{ id: 'rfx-mcp-018', match: /glob\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: restrict glob pattern */ glob(params.', lang: 'any', tier: 2, title: 'Flag unrestricted glob in MCP handler' },
|
|
22
|
+
{ id: 'rfx-mcp-019', match: /fetch\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: SSRF — validate URL */ fetch(params.', lang: 'any', tier: 2, title: 'Flag SSRF in MCP handler' },
|
|
23
|
+
{ id: 'rfx-mcp-020', match: /redirect\(\s*(?:params|args|input)\./g, replace: '/* SECURITY: open redirect */ redirect(params.', lang: 'any', tier: 2, title: 'Flag open redirect in MCP handler' },
|
|
24
|
+
{ id: 'rfx-mcp-021', match: /allowedTools:\s*\[\s*['"]?\*['"]?\s*\]/g, replace: 'allowedTools: [/* SECURITY: list specific tools, not wildcard */]', lang: 'any', tier: 1, title: 'Restrict MCP tool wildcard' },
|
|
25
|
+
{ id: 'rfx-mcp-022', match: /maxTokens:\s*Infinity/g, replace: 'maxTokens: 4096', lang: 'any', tier: 1, title: 'Set MCP response token limit' },
|
|
26
|
+
{ id: 'rfx-mcp-023', match: /timeout:\s*0\b/g, replace: 'timeout: 30000', lang: 'any', tier: 1, title: 'Set MCP handler timeout' },
|
|
27
|
+
{ id: 'rfx-mcp-024', match: /server\.connect\(\s*transport\s*\)/g, replace: 'server.connect(transport) /* ensure error handler is set */', lang: 'any', tier: 2, title: 'Add error handler to MCP server' },
|
|
28
|
+
{ id: 'rfx-mcp-025', match: /schema:\s*\{\s*type:\s*['"]object['"]\s*\}/g, replace: 'schema: { type: "object", properties: { /* define expected params */ }, required: [] }', lang: 'any', tier: 2, title: 'Define MCP tool schema properties' },
|
|
29
|
+
|
|
30
|
+
// --- AI API Fixes (26-50) ---
|
|
31
|
+
{ id: 'rfx-ai-001', match: /(?:OPENAI_API_KEY|openai_api_key)\s*[:=]\s*['"]sk-[a-zA-Z0-9]{20,}['"]/g, replace: (m) => m.replace(/['"]sk-[a-zA-Z0-9]+['"]/, 'process.env.OPENAI_API_KEY'), lang: 'any', tier: 1, title: 'Move OpenAI key to env var' },
|
|
32
|
+
{ id: 'rfx-ai-002', match: /(?:ANTHROPIC_API_KEY|anthropic_api_key)\s*[:=]\s*['"]sk-ant-[a-zA-Z0-9]{20,}['"]/g, replace: (m) => m.replace(/['"]sk-ant-[a-zA-Z0-9]+['"]/, 'process.env.ANTHROPIC_API_KEY'), lang: 'any', tier: 1, title: 'Move Anthropic key to env var' },
|
|
33
|
+
{ id: 'rfx-ai-003', match: /apiKey:\s*['"]sk-[a-zA-Z0-9]{20,}['"]/g, replace: 'apiKey: process.env.OPENAI_API_KEY', lang: 'any', tier: 1, title: 'Move API key to env var' },
|
|
34
|
+
{ id: 'rfx-ai-004', match: /new\s+OpenAI\(\s*\{\s*apiKey:\s*['"][^'"]+['"]/g, replace: 'new OpenAI({ apiKey: process.env.OPENAI_API_KEY', lang: 'any', tier: 1, title: 'Use env var for OpenAI client' },
|
|
35
|
+
{ id: 'rfx-ai-005', match: /new\s+Anthropic\(\s*\{\s*apiKey:\s*['"][^'"]+['"]/g, replace: 'new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY', lang: 'any', tier: 1, title: 'Use env var for Anthropic client' },
|
|
36
|
+
{ id: 'rfx-ai-006', match: /chat\.completions\.create\(\s*\{(?![^}]*max_tokens)/g, replace: 'chat.completions.create({ max_tokens: 1024, ', lang: 'any', tier: 1, title: 'Add max_tokens to OpenAI completion' },
|
|
37
|
+
{ id: 'rfx-ai-007', match: /messages\.create\(\s*\{(?![^}]*max_tokens)/g, replace: 'messages.create({ max_tokens: 1024, ', lang: 'any', tier: 1, title: 'Add max_tokens to Anthropic message' },
|
|
38
|
+
{ id: 'rfx-ai-008', match: /model:\s*['"]gpt-4['"](?!\s*[-,])/g, replace: 'model: "gpt-4-turbo-preview"', lang: 'any', tier: 1, title: 'Pin GPT-4 model version' },
|
|
39
|
+
{ id: 'rfx-ai-009', match: /model:\s*['"]gpt-3\.5-turbo['"](?!\s*[-,])/g, replace: 'model: "gpt-3.5-turbo-0125"', lang: 'any', tier: 1, title: 'Pin GPT-3.5 model version' },
|
|
40
|
+
{ id: 'rfx-ai-010', match: /temperature:\s*([2-9]|1\.\d*[1-9])/g, replace: 'temperature: 1.0', lang: 'any', tier: 1, title: 'Cap AI temperature to 1.0' },
|
|
41
|
+
{ id: 'rfx-ai-011', match: /console\.log\(.*(?:prompt|completion|response\.data)/gi, replace: '/* SECURITY: do not log AI prompts/responses in production */', lang: 'any', tier: 1, title: 'Remove AI prompt/response logging' },
|
|
42
|
+
{ id: 'rfx-ai-012', match: /content:\s*`[^`]*\$\{(?:req\.|request\.|user_input|input)/g, replace: (m) => `/* SECURITY: sanitize user input before including in prompt */ ${m}`, lang: 'any', tier: 2, title: 'Flag prompt injection risk' },
|
|
43
|
+
{ id: 'rfx-ai-013', match: /messages:\s*\[\s*\{\s*role:\s*['"]system['"]\s*,\s*content:\s*`[^`]*password/gi, replace: (m) => `/* SECURITY: sensitive data in system prompt */ ${m}`, lang: 'any', tier: 2, title: 'Flag sensitive data in system prompt' },
|
|
44
|
+
{ id: 'rfx-ai-014', match: /\.create\(\s*\{[^}]*\}\s*\)(?!\s*\.catch|\s*\.then)/g, replace: (m) => `${m}.catch(err => { console.error("AI API error:", err.message); throw err; })`, lang: 'any', tier: 1, title: 'Add error handling to AI API call' },
|
|
45
|
+
{ id: 'rfx-ai-015', match: /fetch\(\s*['"]http:\/\/api\.openai/g, replace: 'fetch("https://api.openai', lang: 'any', tier: 1, title: 'Use HTTPS for OpenAI API' },
|
|
46
|
+
{ id: 'rfx-ai-016', match: /Authorization.*Bearer\s+sk-[a-zA-Z0-9]{20,}/g, replace: (m) => m.replace(/sk-[a-zA-Z0-9]+/, '${process.env.OPENAI_API_KEY}'), lang: 'any', tier: 1, title: 'Move Bearer token to env var' },
|
|
47
|
+
{ id: 'rfx-ai-017', match: /(?:ssn|social_security|credit_card|card_number).*(?:chat\.completions|messages\.create)/gi, replace: (m) => `/* SECURITY: PII sent to AI API — redact first */ ${m}`, lang: 'any', tier: 2, title: 'Flag PII in AI API calls' },
|
|
48
|
+
{ id: 'rfx-ai-018', match: /\.create\(\s*\{[^}]*stream:\s*true(?![^}]*signal)/g, replace: (m) => `/* add AbortController signal for streaming timeout */ ${m}`, lang: 'any', tier: 2, title: 'Add timeout to AI streaming' },
|
|
49
|
+
{ id: 'rfx-ai-019', match: /GOOGLE_AI_KEY\s*[:=]\s*['"][A-Za-z0-9_-]{20,}['"]/g, replace: (m) => m.replace(/['"][A-Za-z0-9_-]+['"]/, 'process.env.GOOGLE_AI_KEY'), lang: 'any', tier: 1, title: 'Move Google AI key to env var' },
|
|
50
|
+
{ id: 'rfx-ai-020', match: /AZURE_OPENAI_KEY\s*[:=]\s*['"][a-f0-9]{20,}['"]/g, replace: (m) => m.replace(/['"][a-f0-9]+['"]/, 'process.env.AZURE_OPENAI_KEY'), lang: 'any', tier: 1, title: 'Move Azure OpenAI key to env var' },
|
|
51
|
+
{ id: 'rfx-ai-021', match: /while\s*\(\s*true\s*\)\s*\{[^}]*\.create\(/g, replace: (m) => `/* SECURITY: add iteration limit to prevent runaway costs */ ${m}`, lang: 'any', tier: 2, title: 'Flag unbounded AI API loop' },
|
|
52
|
+
{ id: 'rfx-ai-022', match: /function_call|tool_choice:\s*['"]auto['"]/g, replace: (m) => `/* review: auto tool/function calling can be exploited */ ${m}`, lang: 'any', tier: 2, title: 'Flag auto function calling' },
|
|
53
|
+
{ id: 'rfx-ai-023', match: /store:\s*true/g, replace: 'store: false /* do not store conversations unless required */', lang: 'any', tier: 1, title: 'Disable AI conversation storage' },
|
|
54
|
+
{ id: 'rfx-ai-024', match: /verbose:\s*true|debug:\s*true/g, replace: (m) => `/* disable in production */ ${m.replace('true', 'process.env.NODE_ENV !== "production"')}`, lang: 'any', tier: 1, title: 'Disable AI debug mode in production' },
|
|
55
|
+
{ id: 'rfx-ai-025', match: /openai\.(?:chat|completions|embeddings)\.\w+\(/g, replace: (m) => m, lang: 'any', tier: 1, title: 'Confirm AI API call pattern' },
|
|
56
|
+
];
|
|
57
|
+
export default fixes;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Data-driven Python auto-fixes (70 entries)
|
|
2
|
+
const fixes = [
|
|
3
|
+
// --- SQL Injection (1-10) ---
|
|
4
|
+
{ id: 'rfx-py-001', match: /execute\(\s*f"SELECT\s+.*\{(\w+)\}/g, replace: 'execute("SELECT ... WHERE col = ?", ($1,)', lang: 'py', tier: 2, title: 'Parameterize f-string SQL query' },
|
|
5
|
+
{ id: 'rfx-py-002', match: /execute\(\s*"SELECT\s+[^"]*"\s*%\s*(\w+)/g, replace: 'execute("SELECT ... WHERE col = ?", ($1,))', lang: 'py', tier: 2, title: 'Parameterize %-format SQL query' },
|
|
6
|
+
{ id: 'rfx-py-003', match: /execute\(\s*"SELECT\s+[^"]*"\s*\+\s*(\w+)/g, replace: 'execute("SELECT ... WHERE col = ?", ($1,))', lang: 'py', tier: 2, title: 'Parameterize concat SQL query' },
|
|
7
|
+
{ id: 'rfx-py-004', match: /\.execute\(\s*f"INSERT\s+.*\{(\w+)\}/g, replace: '.execute("INSERT ... VALUES (?)", ($1,))', lang: 'py', tier: 2, title: 'Parameterize f-string INSERT' },
|
|
8
|
+
{ id: 'rfx-py-005', match: /\.execute\(\s*f"UPDATE\s+.*\{(\w+)\}/g, replace: '.execute("UPDATE ... SET col = ?", ($1,))', lang: 'py', tier: 2, title: 'Parameterize f-string UPDATE' },
|
|
9
|
+
{ id: 'rfx-py-006', match: /\.execute\(\s*f"DELETE\s+.*\{(\w+)\}/g, replace: '.execute("DELETE ... WHERE col = ?", ($1,))', lang: 'py', tier: 2, title: 'Parameterize f-string DELETE' },
|
|
10
|
+
{ id: 'rfx-py-007', match: /cursor\.execute\(\s*['"].*['"]\.format\(/g, replace: (m) => `# SECURITY: use parameterized queries instead of .format()\n${m}`, lang: 'py', tier: 2, title: 'Flag .format() in SQL query' },
|
|
11
|
+
{ id: 'rfx-py-008', match: /\.raw\(\s*f"SELECT/g, replace: (m) => `# SECURITY: use .raw() with params instead of f-string\n${m}`, lang: 'py', tier: 2, title: 'Flag Django raw() f-string SQL' },
|
|
12
|
+
{ id: 'rfx-py-009', match: /\.extra\(\s*where\s*=\s*\[.*%s/g, replace: (m) => `# SECURITY: prefer ORM filters over .extra(where=)\n${m}`, lang: 'py', tier: 2, title: 'Flag Django extra() raw SQL' },
|
|
13
|
+
{ id: 'rfx-py-010', match: /text\(\s*f"SELECT/g, replace: (m) => `# SECURITY: use text() with bindparams\n${m}`, lang: 'py', tier: 2, title: 'Flag SQLAlchemy text() f-string' },
|
|
14
|
+
|
|
15
|
+
// --- Command/Code Injection (11-20) ---
|
|
16
|
+
{ id: 'rfx-py-011', match: /os\.system\(\s*f?['"]([^'"]*)\{(\w+)\}/g, replace: 'subprocess.run(["$1", $2], shell=False)', lang: 'py', tier: 2, title: 'Replace os.system with subprocess.run' },
|
|
17
|
+
{ id: 'rfx-py-012', match: /os\.system\(/g, replace: 'subprocess.run(', lang: 'py', tier: 2, title: 'Replace os.system with subprocess.run' },
|
|
18
|
+
{ id: 'rfx-py-013', match: /os\.popen\(/g, replace: 'subprocess.run(', lang: 'py', tier: 2, title: 'Replace os.popen with subprocess.run' },
|
|
19
|
+
{ id: 'rfx-py-014', match: /subprocess\.call\(([^,]+),\s*shell\s*=\s*True/g, replace: 'subprocess.call(shlex.split($1), shell=False', lang: 'py', tier: 2, title: 'Replace shell=True with shlex.split' },
|
|
20
|
+
{ id: 'rfx-py-015', match: /subprocess\.Popen\(([^,]+),\s*shell\s*=\s*True/g, replace: 'subprocess.Popen(shlex.split($1), shell=False', lang: 'py', tier: 2, title: 'Replace Popen shell=True with shlex.split' },
|
|
21
|
+
{ id: 'rfx-py-016', match: /\beval\(\s*input\(/g, replace: '# SECURITY: never eval user input\nast.literal_eval(input(', lang: 'py', tier: 2, title: 'Replace eval(input()) with ast.literal_eval' },
|
|
22
|
+
{ id: 'rfx-py-017', match: /\beval\(\s*request\./g, replace: '# SECURITY: never eval request data\nast.literal_eval(request.', lang: 'py', tier: 2, title: 'Replace eval(request.data) with ast.literal_eval' },
|
|
23
|
+
{ id: 'rfx-py-018', match: /\bexec\(\s*request\./g, replace: '# SECURITY: never exec request data — remove this\nexec(request.', lang: 'py', tier: 2, title: 'Flag exec(request.data)' },
|
|
24
|
+
{ id: 'rfx-py-019', match: /pickle\.loads?\(/g, replace: 'json.loads(', lang: 'py', tier: 2, title: 'Replace pickle.load with json.loads' },
|
|
25
|
+
{ id: 'rfx-py-020', match: /marshal\.loads?\(/g, replace: 'json.loads(', lang: 'py', tier: 2, title: 'Replace marshal.load with json.loads' },
|
|
26
|
+
|
|
27
|
+
// --- Crypto (21-30) ---
|
|
28
|
+
{ id: 'rfx-py-021', match: /hashlib\.md5\(/g, replace: 'hashlib.sha256(', lang: 'py', tier: 1, title: 'Replace MD5 with SHA-256' },
|
|
29
|
+
{ id: 'rfx-py-022', match: /hashlib\.sha1\(/g, replace: 'hashlib.sha256(', lang: 'py', tier: 1, title: 'Replace SHA-1 with SHA-256' },
|
|
30
|
+
{ id: 'rfx-py-023', match: /yaml\.load\(\s*([^,)]+)\s*\)/g, replace: 'yaml.safe_load($1)', lang: 'py', tier: 1, title: 'Replace yaml.load with yaml.safe_load' },
|
|
31
|
+
{ id: 'rfx-py-024', match: /yaml\.load\(\s*([^,]+),\s*Loader\s*=\s*yaml\.Loader/g, replace: 'yaml.safe_load($1', lang: 'py', tier: 1, title: 'Replace yaml.Loader with safe_load' },
|
|
32
|
+
{ id: 'rfx-py-025', match: /from\s+xml\.etree\s+import\s+ElementTree/g, replace: 'from defusedxml.ElementTree import parse, fromstring', lang: 'py', tier: 1, title: 'Replace xml.etree with defusedxml' },
|
|
33
|
+
{ id: 'rfx-py-026', match: /xml\.etree\.ElementTree\.parse/g, replace: 'defusedxml.ElementTree.parse', lang: 'py', tier: 1, title: 'Replace ElementTree.parse with defusedxml' },
|
|
34
|
+
{ id: 'rfx-py-027', match: /DES\.new\(/g, replace: 'AES.new(', lang: 'py', tier: 1, title: 'Replace DES with AES' },
|
|
35
|
+
{ id: 'rfx-py-028', match: /AES\.MODE_ECB/g, replace: 'AES.MODE_GCM', lang: 'py', tier: 1, title: 'Replace AES ECB mode with GCM' },
|
|
36
|
+
{ id: 'rfx-py-029', match: /random\.random\(\)/g, replace: 'secrets.token_hex(16)', lang: 'py', tier: 1, title: 'Replace random.random with secrets.token_hex' },
|
|
37
|
+
{ id: 'rfx-py-030', match: /random\.randint\(/g, replace: 'secrets.randbelow(', lang: 'py', tier: 1, title: 'Replace random.randint with secrets.randbelow' },
|
|
38
|
+
|
|
39
|
+
// --- Django/Flask Security (31-40) ---
|
|
40
|
+
{ id: 'rfx-py-031', match: /DEBUG\s*=\s*True/g, replace: 'DEBUG = os.environ.get("DEBUG", "False").lower() == "true"', lang: 'py', tier: 1, title: 'Set DEBUG from environment variable' },
|
|
41
|
+
{ id: 'rfx-py-032', match: /SECRET_KEY\s*=\s*['"][^'"]{1,50}['"]/g, replace: 'SECRET_KEY = os.environ["SECRET_KEY"]', lang: 'py', tier: 1, title: 'Move SECRET_KEY to environment variable' },
|
|
42
|
+
{ id: 'rfx-py-033', match: /ALLOWED_HOSTS\s*=\s*\[\s*['"]?\*['"]?\s*\]/g, replace: 'ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")', lang: 'py', tier: 1, title: 'Restrict ALLOWED_HOSTS from wildcard' },
|
|
43
|
+
{ id: 'rfx-py-034', match: /SESSION_COOKIE_SECURE\s*=\s*False/g, replace: 'SESSION_COOKIE_SECURE = True', lang: 'py', tier: 1, title: 'Enable SESSION_COOKIE_SECURE' },
|
|
44
|
+
{ id: 'rfx-py-035', match: /CSRF_COOKIE_SECURE\s*=\s*False/g, replace: 'CSRF_COOKIE_SECURE = True', lang: 'py', tier: 1, title: 'Enable CSRF_COOKIE_SECURE' },
|
|
45
|
+
{ id: 'rfx-py-036', match: /SESSION_COOKIE_HTTPONLY\s*=\s*False/g, replace: 'SESSION_COOKIE_HTTPONLY = True', lang: 'py', tier: 1, title: 'Enable SESSION_COOKIE_HTTPONLY' },
|
|
46
|
+
{ id: 'rfx-py-037', match: /SECURE_SSL_REDIRECT\s*=\s*False/g, replace: 'SECURE_SSL_REDIRECT = True', lang: 'py', tier: 1, title: 'Enable SECURE_SSL_REDIRECT' },
|
|
47
|
+
{ id: 'rfx-py-038', match: /SECURE_HSTS_SECONDS\s*=\s*0/g, replace: 'SECURE_HSTS_SECONDS = 31536000', lang: 'py', tier: 1, title: 'Set HSTS to 1 year' },
|
|
48
|
+
{ id: 'rfx-py-039', match: /app\.run\(\s*debug\s*=\s*True/g, replace: 'app.run(debug=os.environ.get("FLASK_DEBUG", "0") == "1"', lang: 'py', tier: 1, title: 'Set Flask debug from env var' },
|
|
49
|
+
{ id: 'rfx-py-040', match: /app\.secret_key\s*=\s*['"][^'"]+['"]/g, replace: 'app.secret_key = os.environ["SECRET_KEY"]', lang: 'py', tier: 1, title: 'Move Flask secret_key to env var' },
|
|
50
|
+
|
|
51
|
+
// --- Input/Path Security (41-50) ---
|
|
52
|
+
{ id: 'rfx-py-041', match: /open\(\s*request\.(GET|POST|data)\[/g, replace: '# SECURITY: validate file path before opening\nopen(os.path.basename(request.$1[', lang: 'py', tier: 2, title: 'Add path validation for file open' },
|
|
53
|
+
{ id: 'rfx-py-042', match: /os\.path\.join\(\s*\w+,\s*request\./g, replace: (m) => `# SECURITY: validate path component\n${m}`, lang: 'py', tier: 2, title: 'Flag path.join with request data' },
|
|
54
|
+
{ id: 'rfx-py-043', match: /tempfile\.mktemp\(/g, replace: 'tempfile.mkstemp(', lang: 'py', tier: 1, title: 'Replace mktemp with mkstemp (race condition)' },
|
|
55
|
+
{ id: 'rfx-py-044', match: /verify\s*=\s*False/g, replace: 'verify=True', lang: 'py', tier: 1, title: 'Enable SSL verification' },
|
|
56
|
+
{ id: 'rfx-py-045', match: /requests\.get\(\s*request\./g, replace: '# SECURITY: SSRF risk — validate URL\nrequests.get(request.', lang: 'py', tier: 2, title: 'Flag SSRF in requests.get' },
|
|
57
|
+
{ id: 'rfx-py-046', match: /urllib\.request\.urlopen\(\s*request\./g, replace: '# SECURITY: SSRF risk — validate URL\nurllib.request.urlopen(request.', lang: 'py', tier: 2, title: 'Flag SSRF in urlopen' },
|
|
58
|
+
{ id: 'rfx-py-047', match: /send_file\(\s*request\./g, replace: '# SECURITY: path traversal — validate filename\nsend_file(secure_filename(request.', lang: 'py', tier: 2, title: 'Add secure_filename to send_file' },
|
|
59
|
+
{ id: 'rfx-py-048', match: /os\.chmod\([^,]+,\s*0o?777\)/g, replace: (m) => m.replace(/0o?777/, '0o755'), lang: 'py', tier: 1, title: 'Reduce file permissions from 777 to 755' },
|
|
60
|
+
{ id: 'rfx-py-049', match: /shutil\.copy\(\s*request\./g, replace: '# SECURITY: validate source path\nshutil.copy(request.', lang: 'py', tier: 2, title: 'Flag shutil.copy with user input' },
|
|
61
|
+
{ id: 'rfx-py-050', match: /os\.makedirs\(\s*request\./g, replace: '# SECURITY: validate directory path\nos.makedirs(request.', lang: 'py', tier: 2, title: 'Flag makedirs with user input' },
|
|
62
|
+
|
|
63
|
+
// --- Auth/Session (51-60) ---
|
|
64
|
+
{ id: 'rfx-py-051', match: /password\s*==\s*['"][^'"]+['"]/g, replace: '# SECURITY: never compare passwords in plaintext — use bcrypt/argon2', lang: 'py', tier: 2, title: 'Flag plaintext password comparison' },
|
|
65
|
+
{ id: 'rfx-py-052', match: /md5\(.*password/gi, replace: (m) => `# SECURITY: use bcrypt for passwords, not MD5\n${m}`, lang: 'py', tier: 2, title: 'Flag MD5 password hashing' },
|
|
66
|
+
{ id: 'rfx-py-053', match: /sha1\(.*password/gi, replace: (m) => `# SECURITY: use bcrypt for passwords, not SHA-1\n${m}`, lang: 'py', tier: 2, title: 'Flag SHA-1 password hashing' },
|
|
67
|
+
{ id: 'rfx-py-054', match: /jwt\.decode\(\s*([^,]+),\s*([^,]+),\s*algorithms\s*=\s*\[\s*['"]none['"]\s*\]/g, replace: 'jwt.decode($1, $2, algorithms=["HS256"]', lang: 'py', tier: 1, title: 'Remove "none" from JWT algorithms' },
|
|
68
|
+
{ id: 'rfx-py-055', match: /jwt\.decode\(\s*([^,]+),\s*options\s*=\s*\{\s*['"]verify_signature['"]\s*:\s*False/g, replace: 'jwt.decode($1, options={"verify_signature": True', lang: 'py', tier: 1, title: 'Enable JWT signature verification' },
|
|
69
|
+
{ id: 'rfx-py-056', match: /\bapp\.config\['SESSION_PERMANENT'\]\s*=\s*True/g, replace: "app.config['SESSION_PERMANENT'] = False", lang: 'py', tier: 1, title: 'Disable permanent sessions' },
|
|
70
|
+
{ id: 'rfx-py-057', match: /login_required\s*=\s*False/g, replace: 'login_required = True', lang: 'py', tier: 1, title: 'Enable login_required' },
|
|
71
|
+
{ id: 'rfx-py-058', match: /\@csrf_exempt/g, replace: '# SECURITY: review if csrf_exempt is truly needed\n@csrf_exempt', lang: 'py', tier: 2, title: 'Flag @csrf_exempt usage' },
|
|
72
|
+
{ id: 'rfx-py-059', match: /CORS\(\s*app\s*\)/g, replace: 'CORS(app, resources={r"/api/*": {"origins": os.environ.get("ALLOWED_ORIGIN", "http://localhost:3000")}})', lang: 'py', tier: 1, title: 'Restrict Flask-CORS from wildcard' },
|
|
73
|
+
{ id: 'rfx-py-060', match: /allow_all_origins\s*=\s*True/g, replace: 'allow_all_origins = False', lang: 'py', tier: 1, title: 'Disable allow_all_origins in CORS' },
|
|
74
|
+
|
|
75
|
+
// --- Misc (61-70) ---
|
|
76
|
+
{ id: 'rfx-py-061', match: /\bassert\s+request\./g, replace: '# SECURITY: use proper validation, not assert (stripped in production)\nif not request.', lang: 'py', tier: 2, title: 'Replace assert with proper validation' },
|
|
77
|
+
{ id: 'rfx-py-062', match: /logging\.\w+\(.*password/gi, replace: (m) => '# SECURITY: do not log passwords\n' + m.replace(/password\w*/gi, '"[REDACTED]"'), lang: 'py', tier: 1, title: 'Redact passwords in logging' },
|
|
78
|
+
{ id: 'rfx-py-063', match: /logging\.\w+\(.*token/gi, replace: (m) => '# SECURITY: do not log tokens\n' + m.replace(/token\w*/gi, '"[REDACTED]"'), lang: 'py', tier: 1, title: 'Redact tokens in logging' },
|
|
79
|
+
{ id: 'rfx-py-064', match: /print\(.*password/gi, replace: '# SECURITY: do not print passwords', lang: 'py', tier: 1, title: 'Remove password printing' },
|
|
80
|
+
{ id: 'rfx-py-065', match: /re\.compile\(\s*request\./g, replace: '# SECURITY: ReDoS risk — do not compile user input as regex\nre.compile(re.escape(request.', lang: 'py', tier: 2, title: 'Escape user input in regex compilation' },
|
|
81
|
+
{ id: 'rfx-py-066', match: /shelve\.open\(/g, replace: '# SECURITY: shelve uses pickle — consider json\njson.load(open(', lang: 'py', tier: 2, title: 'Replace shelve with json' },
|
|
82
|
+
{ id: 'rfx-py-067', match: /xmlrpc/g, replace: 'xmlrpc', lang: 'py', tier: 1, title: 'Flag xmlrpc usage (XXE risk)' },
|
|
83
|
+
{ id: 'rfx-py-068', match: /X_FRAME_OPTIONS\s*=\s*['"]ALLOWALL['"]/g, replace: "X_FRAME_OPTIONS = 'DENY'", lang: 'py', tier: 1, title: 'Set X-Frame-Options to DENY' },
|
|
84
|
+
{ id: 'rfx-py-069', match: /SECURE_BROWSER_XSS_FILTER\s*=\s*False/g, replace: 'SECURE_BROWSER_XSS_FILTER = True', lang: 'py', tier: 1, title: 'Enable XSS filter' },
|
|
85
|
+
{ id: 'rfx-py-070', match: /SECURE_CONTENT_TYPE_NOSNIFF\s*=\s*False/g, replace: 'SECURE_CONTENT_TYPE_NOSNIFF = True', lang: 'py', tier: 1, title: 'Enable content type nosniff' },
|
|
86
|
+
];
|
|
87
|
+
export default fixes;
|