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,608 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Ruby & PHP auto-fix transforms (70 total: 35 Ruby + 35 PHP)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Each fix function takes a content string and returns the modified string,
|
|
5
|
+
// or null if the fix could not be applied.
|
|
6
|
+
//
|
|
7
|
+
// R1-R10 and P1-P10 are re-exported from fixer.js so that this module is the
|
|
8
|
+
// single source of truth for all Ruby and PHP fixes.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
fixRubyFindBySql,
|
|
13
|
+
fixRubyHtmlSafe,
|
|
14
|
+
fixRubySystemCall,
|
|
15
|
+
fixRubyYamlLoad,
|
|
16
|
+
fixRubyMarshalLoad,
|
|
17
|
+
fixRubyPermitAll,
|
|
18
|
+
fixRubySkipCsrf,
|
|
19
|
+
fixRubyEval as fixRubyEvalInput,
|
|
20
|
+
fixRubyOpenUri,
|
|
21
|
+
fixRubyWeakHash,
|
|
22
|
+
fixPhpMysqlQuery,
|
|
23
|
+
fixPhpEcho as fixPhpEchoUnsafe,
|
|
24
|
+
fixPhpEval,
|
|
25
|
+
fixPhpExec as fixPhpExecInput,
|
|
26
|
+
fixPhpMd5Password,
|
|
27
|
+
fixPhpExtract as fixPhpExtractPost,
|
|
28
|
+
fixPhpLooseComparison,
|
|
29
|
+
fixPhpSessionConfig,
|
|
30
|
+
fixPhpUnserialize,
|
|
31
|
+
fixPhpInclude as fixPhpIncludeInput,
|
|
32
|
+
} from './fixer.js';
|
|
33
|
+
|
|
34
|
+
// Re-export the imported fixes so consumers get everything from this module
|
|
35
|
+
export {
|
|
36
|
+
fixRubyFindBySql,
|
|
37
|
+
fixRubyHtmlSafe,
|
|
38
|
+
fixRubySystemCall,
|
|
39
|
+
fixRubyYamlLoad,
|
|
40
|
+
fixRubyMarshalLoad,
|
|
41
|
+
fixRubyPermitAll,
|
|
42
|
+
fixRubySkipCsrf,
|
|
43
|
+
fixRubyEvalInput,
|
|
44
|
+
fixRubyOpenUri,
|
|
45
|
+
fixRubyWeakHash,
|
|
46
|
+
fixPhpMysqlQuery,
|
|
47
|
+
fixPhpEchoUnsafe,
|
|
48
|
+
fixPhpEval,
|
|
49
|
+
fixPhpExecInput,
|
|
50
|
+
fixPhpMd5Password,
|
|
51
|
+
fixPhpExtractPost,
|
|
52
|
+
fixPhpLooseComparison,
|
|
53
|
+
fixPhpSessionConfig,
|
|
54
|
+
fixPhpUnserialize,
|
|
55
|
+
fixPhpIncludeInput,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ===========================================================================
|
|
59
|
+
// Ruby fixes (R11 – R35)
|
|
60
|
+
// ===========================================================================
|
|
61
|
+
|
|
62
|
+
// R11. Replace hardcoded secret with ENV lookup
|
|
63
|
+
export function fixRubyHardcodedSecret(content) {
|
|
64
|
+
const modified = content.replace(
|
|
65
|
+
/(\w*secret\w*)\s*=\s*["']([^"']{8,})["']/gi,
|
|
66
|
+
"$1 = ENV['SECRET'] # TODO: set SECRET in environment",
|
|
67
|
+
);
|
|
68
|
+
return modified === content ? null : modified;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// R12. Add strong params wrapper where params used directly
|
|
72
|
+
export function fixRubyNoStrongParams(content) {
|
|
73
|
+
const modified = content.replace(
|
|
74
|
+
/(\w+)\.create\(\s*params\s*\)/g,
|
|
75
|
+
"$1.create(permitted_params) # TODO: define permitted_params via params.require(...).permit(...)",
|
|
76
|
+
);
|
|
77
|
+
return modified === content ? null : modified;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// R13. Replace raw output with h() helper
|
|
81
|
+
export function fixRubyRawOutput(content) {
|
|
82
|
+
const modified = content.replace(
|
|
83
|
+
/<%=\s*raw\s+(\w+)\s*%>/g,
|
|
84
|
+
'<%= h($1) %>',
|
|
85
|
+
);
|
|
86
|
+
return modified === content ? null : modified;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// R14. Add Shellwords.shellescape to exec input
|
|
90
|
+
export function fixRubyExecInput(content) {
|
|
91
|
+
const modified = content.replace(
|
|
92
|
+
/`([^`]*?)#\{(\w+)\}([^`]*?)`/g,
|
|
93
|
+
'`$1#{Shellwords.shellescape($2)}$3`',
|
|
94
|
+
);
|
|
95
|
+
return modified === content ? null : modified;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// R15. Replace backticks command with Open3.capture3
|
|
99
|
+
export function fixRubyBackticksInput(content) {
|
|
100
|
+
const modified = content.replace(
|
|
101
|
+
/(\w+)\s*=\s*`([^`]+)`/g,
|
|
102
|
+
'$1_out, $1_err, $1_status = Open3.capture3("$2")',
|
|
103
|
+
);
|
|
104
|
+
return modified === content ? null : modified;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// R16. Replace send() with public_send()
|
|
108
|
+
export function fixRubySendUnsafe(content) {
|
|
109
|
+
const modified = content.replace(
|
|
110
|
+
/\.send\(/g,
|
|
111
|
+
'.public_send(',
|
|
112
|
+
);
|
|
113
|
+
return modified === content ? null : modified;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// R17. Add whitelist check to const_get
|
|
117
|
+
export function fixRubyConstGet(content) {
|
|
118
|
+
const modified = content.replace(
|
|
119
|
+
/const_get\((\w+)\)/g,
|
|
120
|
+
'const_get($1) # SECURITY: validate $1 against an allowlist before calling const_get',
|
|
121
|
+
);
|
|
122
|
+
return modified === content ? null : modified;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// R18. Add timeout to HTTP requests
|
|
126
|
+
export function fixRubyNoTimeout(content) {
|
|
127
|
+
const modified = content.replace(
|
|
128
|
+
/Net::HTTP\.get\(([^)]+)\)/g,
|
|
129
|
+
'Net::HTTP.start($1, open_timeout: 10, read_timeout: 10) { |http| http.get($1) }',
|
|
130
|
+
);
|
|
131
|
+
return modified === content ? null : modified;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// R19. Replace VERIFY_NONE with VERIFY_PEER
|
|
135
|
+
export function fixRubyInsecureSsl(content) {
|
|
136
|
+
const modified = content.replace(
|
|
137
|
+
/OpenSSL::SSL::VERIFY_NONE/g,
|
|
138
|
+
'OpenSSL::SSL::VERIFY_PEER',
|
|
139
|
+
);
|
|
140
|
+
return modified === content ? null : modified;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// R20. Add Rails.logger where puts used in controllers
|
|
144
|
+
export function fixRubyNoLogging(content) {
|
|
145
|
+
if (!/class\s+\w+Controller/.test(content)) return null;
|
|
146
|
+
const modified = content.replace(
|
|
147
|
+
/\bputs\s+/g,
|
|
148
|
+
'Rails.logger.info ',
|
|
149
|
+
);
|
|
150
|
+
return modified === content ? null : modified;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// R21. Replace hardcoded DB credentials with database.yml reference
|
|
154
|
+
export function fixRubyHardcodedDb(content) {
|
|
155
|
+
const modified = content.replace(
|
|
156
|
+
/((?:password|passwd|db_pass)\s*[:=]\s*)["']([^"']+)["']/gi,
|
|
157
|
+
"$1ENV['DATABASE_PASSWORD'] # TODO: move credential to database.yml / Rails credentials",
|
|
158
|
+
);
|
|
159
|
+
return modified === content ? null : modified;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// R22. Replace hardcoded session secret with credentials.yml
|
|
163
|
+
export function fixRubySessionSecret(content) {
|
|
164
|
+
const modified = content.replace(
|
|
165
|
+
/(secret_key_base\s*=\s*)["']([^"']+)["']/g,
|
|
166
|
+
"$1Rails.application.credentials.secret_key_base",
|
|
167
|
+
);
|
|
168
|
+
return modified === content ? null : modified;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// R23. Add rack-attack rate limiting comment
|
|
172
|
+
export function fixRubyNoRateLimit(content) {
|
|
173
|
+
if (!/class\s+\w+Controller/.test(content)) return null;
|
|
174
|
+
if (/rack.attack|throttle|rate.limit/i.test(content)) return null;
|
|
175
|
+
const modified = content.replace(
|
|
176
|
+
/(class\s+\w+Controller\s*<\s*\w+)/,
|
|
177
|
+
"# TODO: add Rack::Attack throttle rules for rate limiting\n$1",
|
|
178
|
+
);
|
|
179
|
+
return modified === content ? null : modified;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// R24. Sanitize path in File.read
|
|
183
|
+
export function fixRubyFileReadInput(content) {
|
|
184
|
+
const modified = content.replace(
|
|
185
|
+
/File\.read\((\w+)\)/g,
|
|
186
|
+
'File.read(File.expand_path($1, Rails.root.join("safe_dir"))) # SECURITY: validate path',
|
|
187
|
+
);
|
|
188
|
+
return modified === content ? null : modified;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// R25. Escape glob pattern input
|
|
192
|
+
export function fixRubyGlobInput(content) {
|
|
193
|
+
const modified = content.replace(
|
|
194
|
+
/Dir\.glob\((\w+)\)/g,
|
|
195
|
+
'Dir.glob($1.gsub(/[\\[\\]\\*\\?]/, "")) # SECURITY: sanitize glob input',
|
|
196
|
+
);
|
|
197
|
+
return modified === content ? null : modified;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// R26. Wrap user input in Regexp.escape
|
|
201
|
+
export function fixRubyRegexpInput(content) {
|
|
202
|
+
const modified = content.replace(
|
|
203
|
+
/Regexp\.new\((\w+)\)/g,
|
|
204
|
+
'Regexp.new(Regexp.escape($1))',
|
|
205
|
+
);
|
|
206
|
+
return modified === content ? null : modified;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// R27. Add before_action :authenticate_user! where missing
|
|
210
|
+
export function fixRubyNoAuth(content) {
|
|
211
|
+
if (!/class\s+\w+Controller/.test(content)) return null;
|
|
212
|
+
if (/before_action\s+:authenticate|skip_before_action\s+:authenticate/.test(content)) return null;
|
|
213
|
+
const modified = content.replace(
|
|
214
|
+
/(class\s+\w+Controller\s*<\s*\w+)/,
|
|
215
|
+
"$1\n before_action :authenticate_user! # TODO: verify authentication requirement",
|
|
216
|
+
);
|
|
217
|
+
return modified === content ? null : modified;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// R28. Replace render inline: with template rendering
|
|
221
|
+
export function fixRubyRenderInline(content) {
|
|
222
|
+
const modified = content.replace(
|
|
223
|
+
/render\s+inline:\s*["']([^"']+)["']/g,
|
|
224
|
+
'render template: "shared/inline_template" # SECURITY: replaced inline render — move markup to a template',
|
|
225
|
+
);
|
|
226
|
+
return modified === content ? null : modified;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// R29. Add URL whitelist to redirect
|
|
230
|
+
export function fixRubyRedirectInput(content) {
|
|
231
|
+
const modified = content.replace(
|
|
232
|
+
/redirect_to\s+(\w+)\b(?!\s*_path|\s*_url)/g,
|
|
233
|
+
'redirect_to($1 =~ /\\A\\// ? $1 : root_path) # SECURITY: validate redirect target',
|
|
234
|
+
);
|
|
235
|
+
return modified === content ? null : modified;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// R30. Replace ENV direct access with Rails.credentials
|
|
239
|
+
export function fixRubyNoEncryptedCreds(content) {
|
|
240
|
+
const modified = content.replace(
|
|
241
|
+
/ENV\[['"](\w*(?:SECRET|KEY|TOKEN)\w*)['"]\]/g,
|
|
242
|
+
'Rails.application.credentials.$1 # TODO: add to credentials.yml.enc',
|
|
243
|
+
);
|
|
244
|
+
return modified === content ? null : modified;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// R31. Replace deprecated find(:all) with where().first
|
|
248
|
+
export function fixRubyDeprecatedFind(content) {
|
|
249
|
+
const modified = content.replace(
|
|
250
|
+
/\.find\(:all,\s*conditions:\s*\[([^\]]+)\]\)/g,
|
|
251
|
+
'.where($1)',
|
|
252
|
+
);
|
|
253
|
+
return modified === content ? null : modified;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// R32. Replace mass assignment with explicit attributes
|
|
257
|
+
export function fixRubyMassAssignment(content) {
|
|
258
|
+
const modified = content.replace(
|
|
259
|
+
/\.update_attributes\(params\[:\w+\]\)/g,
|
|
260
|
+
'.update(permitted_params) # TODO: use strong parameters — params.require(:model).permit(:field1, :field2)',
|
|
261
|
+
);
|
|
262
|
+
return modified === content ? null : modified;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// R33. Add validates presence to model
|
|
266
|
+
export function fixRubyNoValidation(content) {
|
|
267
|
+
if (!/class\s+\w+\s*<\s*(?:ActiveRecord::Base|ApplicationRecord)/.test(content)) return null;
|
|
268
|
+
if (/validates\s/.test(content)) return null;
|
|
269
|
+
const modified = content.replace(
|
|
270
|
+
/(class\s+\w+\s*<\s*(?:ActiveRecord::Base|ApplicationRecord))/,
|
|
271
|
+
"$1\n # TODO: add model validations\n # validates :name, presence: true",
|
|
272
|
+
);
|
|
273
|
+
return modified === content ? null : modified;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// R34. Replace hardcoded email with config setting
|
|
277
|
+
export function fixRubyHardcodedEmail(content) {
|
|
278
|
+
const modified = content.replace(
|
|
279
|
+
/((?:from|to|email)\s*[:=]\s*)["'](\w+@\w+\.\w+)["']/gi,
|
|
280
|
+
"$1Rails.configuration.default_email # TODO: configure email in application.rb",
|
|
281
|
+
);
|
|
282
|
+
return modified === content ? null : modified;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// R35. Add rescue_from for error handling
|
|
286
|
+
export function fixRubyNoErrorHandler(content) {
|
|
287
|
+
if (!/class\s+\w+Controller/.test(content)) return null;
|
|
288
|
+
if (/rescue_from/.test(content)) return null;
|
|
289
|
+
const modified = content.replace(
|
|
290
|
+
/(class\s+(\w+Controller)\s*<\s*\w+)/,
|
|
291
|
+
"$1\n rescue_from StandardError, with: :handle_error\n\n private\n\n def handle_error(e)\n Rails.logger.error(e.message)\n render json: { error: 'Internal server error' }, status: :internal_server_error\n end",
|
|
292
|
+
);
|
|
293
|
+
return modified === content ? null : modified;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ===========================================================================
|
|
297
|
+
// PHP fixes (P11 – P35)
|
|
298
|
+
// ===========================================================================
|
|
299
|
+
|
|
300
|
+
// P11. Add escapeshellcmd to system() calls
|
|
301
|
+
export function fixPhpSystemCall(content) {
|
|
302
|
+
const modified = content.replace(
|
|
303
|
+
/system\(\s*\$(\w+)\s*\)/g,
|
|
304
|
+
'system(escapeshellcmd($$1))',
|
|
305
|
+
);
|
|
306
|
+
return modified === content ? null : modified;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// P12. Replace passthru with proc_open
|
|
310
|
+
export function fixPhpPassthru(content) {
|
|
311
|
+
const modified = content.replace(
|
|
312
|
+
/passthru\(\s*\$(\w+)\s*\)/g,
|
|
313
|
+
'/* SECURITY: replaced passthru with proc_open */\n$proc = proc_open(escapeshellcmd($$1), [1 => ["pipe", "w"]], $pipes); echo stream_get_contents($pipes[1]); proc_close($proc)',
|
|
314
|
+
);
|
|
315
|
+
return modified === content ? null : modified;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// P13. Replace preg_replace with /e modifier with preg_replace_callback
|
|
319
|
+
export function fixPhpPreg_e(content) {
|
|
320
|
+
const modified = content.replace(
|
|
321
|
+
/preg_replace\(\s*(['"])([^'"]*?)e(['"]),\s*(['"][^'"]*?['"])/g,
|
|
322
|
+
'preg_replace_callback($1$2$3, function($m) { return $4; }',
|
|
323
|
+
);
|
|
324
|
+
return modified === content ? null : modified;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// P14. Replace global variable with function parameter
|
|
328
|
+
export function fixPhpGlobalVar(content) {
|
|
329
|
+
const modified = content.replace(
|
|
330
|
+
/\bglobal\s+\$(\w+);/g,
|
|
331
|
+
'/* SECURITY: avoid global — pass $$1 as a function parameter instead */',
|
|
332
|
+
);
|
|
333
|
+
return modified === content ? null : modified;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// P15. Replace magic_quotes reliance with proper escaping
|
|
337
|
+
export function fixPhpMagicQuotes(content) {
|
|
338
|
+
const modified = content.replace(
|
|
339
|
+
/get_magic_quotes_gpc\(\)/g,
|
|
340
|
+
'false /* magic_quotes removed in PHP 7.4 — use prepared statements or proper escaping */',
|
|
341
|
+
);
|
|
342
|
+
return modified === content ? null : modified;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// P16. Replace file_get_contents with curl + validation
|
|
346
|
+
export function fixPhpFileGetContents(content) {
|
|
347
|
+
const modified = content.replace(
|
|
348
|
+
/file_get_contents\(\s*\$(\w+)\s*\)/g,
|
|
349
|
+
'/* SECURITY: validate URL before fetching */ (function() { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, filter_var($$1, FILTER_VALIDATE_URL)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); $r = curl_exec($ch); curl_close($ch); return $r; })()',
|
|
350
|
+
);
|
|
351
|
+
return modified === content ? null : modified;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// P17. Replace hardcoded DB credentials with env variable
|
|
355
|
+
export function fixPhpHardcodedDb(content) {
|
|
356
|
+
const modified = content.replace(
|
|
357
|
+
/((?:password|passwd|db_pass)\s*=\s*)['"]([^'"]+)['"]/gi,
|
|
358
|
+
"$1getenv('DB_PASSWORD') /* TODO: set DB_PASSWORD in .env */",
|
|
359
|
+
);
|
|
360
|
+
return modified === content ? null : modified;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// P18. Add rate-limit middleware comment
|
|
364
|
+
export function fixPhpNoRateLimit(content) {
|
|
365
|
+
if (/throttle|rate.limit|RateLimiter/i.test(content)) return null;
|
|
366
|
+
if (!/Route::/.test(content)) return null;
|
|
367
|
+
const modified = content.replace(
|
|
368
|
+
/(Route::\w+\([^)]+\))/,
|
|
369
|
+
"$1->middleware('throttle:60,1') /* TODO: configure rate limiting */",
|
|
370
|
+
);
|
|
371
|
+
return modified === content ? null : modified;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// P19. Add filter_var to unvalidated input
|
|
375
|
+
export function fixPhpNoInputFilter(content) {
|
|
376
|
+
const modified = content.replace(
|
|
377
|
+
/\$_GET\[['"](\w+)['"]\]/g,
|
|
378
|
+
"filter_input(INPUT_GET, '$1', FILTER_SANITIZE_SPECIAL_CHARS)",
|
|
379
|
+
);
|
|
380
|
+
return modified === content ? null : modified;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// P20. Replace rand() with random_bytes()
|
|
384
|
+
export function fixPhpWeakRandom(content) {
|
|
385
|
+
const modified = content.replace(
|
|
386
|
+
/\brand\(\s*\)/g,
|
|
387
|
+
'random_int(0, PHP_INT_MAX) /* SECURITY: use cryptographically secure random */',
|
|
388
|
+
);
|
|
389
|
+
return modified === content ? null : modified;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// P21. Add type hints to function parameters
|
|
393
|
+
export function fixPhpNoTypeHints(content) {
|
|
394
|
+
const modified = content.replace(
|
|
395
|
+
/function\s+(\w+)\(\s*\$(\w+)\s*\)/g,
|
|
396
|
+
'function $1(mixed $$2) /* TODO: replace mixed with proper type hint */',
|
|
397
|
+
);
|
|
398
|
+
return modified === content ? null : modified;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// P22. Add CSRF token to form
|
|
402
|
+
export function fixPhpNoCsrf(content) {
|
|
403
|
+
const modified = content.replace(
|
|
404
|
+
/(<form\s[^>]*method\s*=\s*["']post["'][^>]*>)/gi,
|
|
405
|
+
'$1\n <?php echo csrf_token(); ?> <!-- SECURITY: add CSRF protection -->',
|
|
406
|
+
);
|
|
407
|
+
return modified === content ? null : modified;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// P23. Replace {!! !!} with {{ }} in Blade templates
|
|
411
|
+
export function fixPhpBladUnescaped(content) {
|
|
412
|
+
const modified = content.replace(
|
|
413
|
+
/\{!!\s*(\$\w+)\s*!!\}/g,
|
|
414
|
+
'{{ $1 }}',
|
|
415
|
+
);
|
|
416
|
+
return modified === content ? null : modified;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// P24. Replace DB::raw with Eloquent query builder
|
|
420
|
+
export function fixPhpLaravelRawQuery(content) {
|
|
421
|
+
const modified = content.replace(
|
|
422
|
+
/DB::select\(\s*DB::raw\(\s*"([^"]*?)\{\$(\w+)\}([^"]*?)"\s*\)\s*\)/g,
|
|
423
|
+
'DB::select("$1?$3", [$$2])',
|
|
424
|
+
);
|
|
425
|
+
return modified === content ? null : modified;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// P25. Add validate() call to controller method
|
|
429
|
+
export function fixPhpNoValidation(content) {
|
|
430
|
+
const modified = content.replace(
|
|
431
|
+
/(\$request->(?:input|all)\(\))/g,
|
|
432
|
+
'$request->validate([/* TODO: add validation rules */]) /* SECURITY: validate input */',
|
|
433
|
+
);
|
|
434
|
+
return modified === content ? null : modified;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// P26. Remove phpinfo() calls
|
|
438
|
+
export function fixPhpEnvExposure(content) {
|
|
439
|
+
const modified = content.replace(
|
|
440
|
+
/\bphpinfo\(\s*\)\s*;?/g,
|
|
441
|
+
'/* SECURITY: phpinfo() removed — exposes server configuration */',
|
|
442
|
+
);
|
|
443
|
+
return modified === content ? null : modified;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// P27. Set APP_DEBUG to false
|
|
447
|
+
export function fixPhpDebugMode(content) {
|
|
448
|
+
const modified = content.replace(
|
|
449
|
+
/APP_DEBUG\s*=\s*true/gi,
|
|
450
|
+
"APP_DEBUG=false /* SECURITY: disable debug mode in production */",
|
|
451
|
+
);
|
|
452
|
+
return modified === content ? null : modified;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// P28. Add open_basedir directive
|
|
456
|
+
export function fixPhpOpenBasedir(content) {
|
|
457
|
+
if (/open_basedir/.test(content)) return null;
|
|
458
|
+
const modified = content.replace(
|
|
459
|
+
/(<\?php)/,
|
|
460
|
+
'$1\nini_set("open_basedir", __DIR__); /* SECURITY: restrict file access to application directory */',
|
|
461
|
+
);
|
|
462
|
+
return modified === content ? null : modified;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// P29. Disable allow_url_include
|
|
466
|
+
export function fixPhpAllowUrlInclude(content) {
|
|
467
|
+
const modified = content.replace(
|
|
468
|
+
/allow_url_include\s*=\s*(?:On|1|true)/gi,
|
|
469
|
+
'allow_url_include = Off ; SECURITY: remote file inclusion disabled',
|
|
470
|
+
);
|
|
471
|
+
return modified === content ? null : modified;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// P30. Disable display_errors
|
|
475
|
+
export function fixPhpNoErrorDisplay(content) {
|
|
476
|
+
const modified = content.replace(
|
|
477
|
+
/display_errors\s*=\s*(?:On|1|true)/gi,
|
|
478
|
+
'display_errors = Off ; SECURITY: do not expose errors to users',
|
|
479
|
+
);
|
|
480
|
+
return modified === content ? null : modified;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// P31. Replace hardcoded secret with getenv()
|
|
484
|
+
export function fixPhpHardcodedSecret(content) {
|
|
485
|
+
const modified = content.replace(
|
|
486
|
+
/((?:secret|api_key|app_key)\s*=\s*)['"]([^'"]{8,})['"]/gi,
|
|
487
|
+
"$1getenv('APP_SECRET') /* TODO: set APP_SECRET in .env */",
|
|
488
|
+
);
|
|
489
|
+
return modified === content ? null : modified;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// P32. Validate mime type and extension on upload
|
|
493
|
+
export function fixPhpInsecureUpload(content) {
|
|
494
|
+
const modified = content.replace(
|
|
495
|
+
/move_uploaded_file\(\s*\$_FILES\[['"](\w+)['"]\]\['tmp_name'\],\s*([^)]+)\)/g,
|
|
496
|
+
"/* SECURITY: validate upload before moving */\n$allowed = ['image/jpeg','image/png','application/pdf'];\nif (in_array($_FILES['$1']['type'], \\$allowed) && preg_match('/\\\\.(jpg|png|pdf)$/i', $_FILES['$1']['name'])) {\n move_uploaded_file($_FILES['$1']['tmp_name'], $2);\n}",
|
|
497
|
+
);
|
|
498
|
+
return modified === content ? null : modified;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// P33. Add ob_start() for output buffering
|
|
502
|
+
export function fixPhpNoOutputBuffering(content) {
|
|
503
|
+
if (/ob_start/.test(content)) return null;
|
|
504
|
+
const modified = content.replace(
|
|
505
|
+
/(<\?php)/,
|
|
506
|
+
'$1\nob_start(); /* Enable output buffering */',
|
|
507
|
+
);
|
|
508
|
+
return modified === content ? null : modified;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// P34. Replace direct SQL string with query builder
|
|
512
|
+
export function fixPhpDirectSql(content) {
|
|
513
|
+
const modified = content.replace(
|
|
514
|
+
/\$\w+->query\(\s*"([^"]*?)\$(\w+)([^"]*?)"\s*\)/g,
|
|
515
|
+
'$$2_stmt = $$1->prepare("$1?$3"); $$2_stmt->execute([$$2])',
|
|
516
|
+
);
|
|
517
|
+
return modified === content ? null : modified;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// P35. Regenerate session ID on login
|
|
521
|
+
export function fixPhpSessionRegenerate(content) {
|
|
522
|
+
if (/session_regenerate_id/.test(content)) return null;
|
|
523
|
+
const modified = content.replace(
|
|
524
|
+
/((?:login|authenticate|sign_in)\s*\([^)]*\)\s*\{)/gi,
|
|
525
|
+
'$1\n session_regenerate_id(true); /* SECURITY: prevent session fixation */',
|
|
526
|
+
);
|
|
527
|
+
return modified === content ? null : modified;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ===========================================================================
|
|
531
|
+
// Registry — maps fix IDs to { fn, tier, title }
|
|
532
|
+
// ===========================================================================
|
|
533
|
+
export const rubyPhpFixes = {
|
|
534
|
+
// Ruby (R1-R10) — re-exported from fixer.js
|
|
535
|
+
'fix-ruby-find-by-sql': { fn: fixRubyFindBySql, tier: 2, title: 'Replace find_by_sql interpolation with parameterized query' },
|
|
536
|
+
'fix-ruby-html-safe': { fn: fixRubyHtmlSafe, tier: 2, title: 'Replace html_safe with sanitize()' },
|
|
537
|
+
'fix-ruby-system-call': { fn: fixRubySystemCall, tier: 2, title: 'Replace system() interpolation with argument list' },
|
|
538
|
+
'fix-ruby-yaml-load': { fn: fixRubyYamlLoad, tier: 1, title: 'Replace YAML.load with YAML.safe_load' },
|
|
539
|
+
'fix-ruby-marshal-load': { fn: fixRubyMarshalLoad, tier: 2, title: 'Add warning to Marshal.load usage' },
|
|
540
|
+
'fix-ruby-permit-all': { fn: fixRubyPermitAll, tier: 2, title: 'Replace permit! with explicit field list' },
|
|
541
|
+
'fix-ruby-skip-csrf': { fn: fixRubySkipCsrf, tier: 2, title: 'Comment out skip_forgery_protection' },
|
|
542
|
+
'fix-ruby-eval': { fn: fixRubyEvalInput, tier: 2, title: 'Replace Ruby eval() with safer alternative' },
|
|
543
|
+
'fix-ruby-open-uri': { fn: fixRubyOpenUri, tier: 1, title: 'Replace open(url) with URI.open(url)' },
|
|
544
|
+
'fix-ruby-weak-hash': { fn: fixRubyWeakHash, tier: 1, title: 'Replace Digest::MD5 with Digest::SHA256' },
|
|
545
|
+
// Ruby (R11-R35)
|
|
546
|
+
'fix-ruby-hardcoded-secret': { fn: fixRubyHardcodedSecret, tier: 1, title: 'Replace hardcoded secret with ENV lookup' },
|
|
547
|
+
'fix-ruby-no-strong-params': { fn: fixRubyNoStrongParams, tier: 2, title: 'Add strong params to create calls' },
|
|
548
|
+
'fix-ruby-raw-output': { fn: fixRubyRawOutput, tier: 2, title: 'Replace raw output with h() helper' },
|
|
549
|
+
'fix-ruby-exec-input': { fn: fixRubyExecInput, tier: 2, title: 'Add Shellwords.shellescape to backtick commands' },
|
|
550
|
+
'fix-ruby-backticks-input': { fn: fixRubyBackticksInput, tier: 2, title: 'Replace backticks with Open3.capture3' },
|
|
551
|
+
'fix-ruby-send-unsafe': { fn: fixRubySendUnsafe, tier: 2, title: 'Replace send() with public_send()' },
|
|
552
|
+
'fix-ruby-const-get': { fn: fixRubyConstGet, tier: 2, title: 'Add allowlist check to const_get' },
|
|
553
|
+
'fix-ruby-no-timeout': { fn: fixRubyNoTimeout, tier: 1, title: 'Add timeout to Net::HTTP requests' },
|
|
554
|
+
'fix-ruby-insecure-ssl': { fn: fixRubyInsecureSsl, tier: 1, title: 'Replace VERIFY_NONE with VERIFY_PEER' },
|
|
555
|
+
'fix-ruby-no-logging': { fn: fixRubyNoLogging, tier: 1, title: 'Replace puts with Rails.logger in controllers' },
|
|
556
|
+
'fix-ruby-hardcoded-db': { fn: fixRubyHardcodedDb, tier: 1, title: 'Replace hardcoded DB password with ENV variable' },
|
|
557
|
+
'fix-ruby-session-secret': { fn: fixRubySessionSecret, tier: 1, title: 'Replace hardcoded session secret with Rails credentials' },
|
|
558
|
+
'fix-ruby-no-rate-limit': { fn: fixRubyNoRateLimit, tier: 2, title: 'Add Rack::Attack rate limiting suggestion' },
|
|
559
|
+
'fix-ruby-file-read-input': { fn: fixRubyFileReadInput, tier: 2, title: 'Sanitize path in File.read calls' },
|
|
560
|
+
'fix-ruby-glob-input': { fn: fixRubyGlobInput, tier: 2, title: 'Escape user input in Dir.glob' },
|
|
561
|
+
'fix-ruby-regexp-input': { fn: fixRubyRegexpInput, tier: 2, title: 'Wrap user input in Regexp.escape' },
|
|
562
|
+
'fix-ruby-no-auth': { fn: fixRubyNoAuth, tier: 2, title: 'Add before_action :authenticate_user!' },
|
|
563
|
+
'fix-ruby-render-inline': { fn: fixRubyRenderInline, tier: 2, title: 'Replace render inline: with template rendering' },
|
|
564
|
+
'fix-ruby-redirect-input': { fn: fixRubyRedirectInput, tier: 2, title: 'Add URL whitelist to redirect_to' },
|
|
565
|
+
'fix-ruby-no-encrypted-creds': { fn: fixRubyNoEncryptedCreds, tier: 1, title: 'Replace ENV secrets with Rails.credentials' },
|
|
566
|
+
'fix-ruby-deprecated-find': { fn: fixRubyDeprecatedFind, tier: 1, title: 'Replace deprecated find(:all) with where()' },
|
|
567
|
+
'fix-ruby-mass-assignment': { fn: fixRubyMassAssignment, tier: 2, title: 'Replace update_attributes(params) with strong params' },
|
|
568
|
+
'fix-ruby-no-validation': { fn: fixRubyNoValidation, tier: 2, title: 'Add model validation stubs' },
|
|
569
|
+
'fix-ruby-hardcoded-email': { fn: fixRubyHardcodedEmail, tier: 1, title: 'Replace hardcoded email with config setting' },
|
|
570
|
+
'fix-ruby-no-error-handler': { fn: fixRubyNoErrorHandler, tier: 2, title: 'Add rescue_from error handler' },
|
|
571
|
+
// PHP (P1-P10) — re-exported from fixer.js
|
|
572
|
+
'fix-php-mysql-query': { fn: fixPhpMysqlQuery, tier: 2, title: 'Replace mysql_query with PDO prepared statement' },
|
|
573
|
+
'fix-php-echo': { fn: fixPhpEchoUnsafe, tier: 1, title: 'Add htmlspecialchars to echoed user input' },
|
|
574
|
+
'fix-php-eval': { fn: fixPhpEval, tier: 2, title: 'Add security warning to PHP eval()' },
|
|
575
|
+
'fix-php-exec': { fn: fixPhpExecInput, tier: 2, title: 'Add escapeshellarg to exec() calls' },
|
|
576
|
+
'fix-php-md5-password': { fn: fixPhpMd5Password, tier: 1, title: 'Replace md5() with password_hash()' },
|
|
577
|
+
'fix-php-extract': { fn: fixPhpExtractPost, tier: 2, title: 'Replace extract() with explicit assignments' },
|
|
578
|
+
'fix-php-loose-comparison': { fn: fixPhpLooseComparison, tier: 1, title: 'Replace == with === in PHP auth contexts' },
|
|
579
|
+
'fix-php-session-config': { fn: fixPhpSessionConfig, tier: 1, title: 'Add secure session cookie settings' },
|
|
580
|
+
'fix-php-unserialize': { fn: fixPhpUnserialize, tier: 2, title: 'Add allowed_classes restriction to unserialize()' },
|
|
581
|
+
'fix-php-include': { fn: fixPhpIncludeInput, tier: 2, title: 'Add warning to include with user input' },
|
|
582
|
+
// PHP (P11-P35)
|
|
583
|
+
'fix-php-system-call': { fn: fixPhpSystemCall, tier: 2, title: 'Add escapeshellcmd to system() calls' },
|
|
584
|
+
'fix-php-passthru': { fn: fixPhpPassthru, tier: 2, title: 'Replace passthru with proc_open' },
|
|
585
|
+
'fix-php-preg-e': { fn: fixPhpPreg_e, tier: 2, title: 'Replace preg_replace /e with preg_replace_callback' },
|
|
586
|
+
'fix-php-global-var': { fn: fixPhpGlobalVar, tier: 2, title: 'Replace global variable with function parameter' },
|
|
587
|
+
'fix-php-magic-quotes': { fn: fixPhpMagicQuotes, tier: 1, title: 'Remove magic_quotes reliance' },
|
|
588
|
+
'fix-php-file-get-contents': { fn: fixPhpFileGetContents, tier: 2, title: 'Replace file_get_contents with curl + validation' },
|
|
589
|
+
'fix-php-hardcoded-db': { fn: fixPhpHardcodedDb, tier: 1, title: 'Replace hardcoded DB password with getenv()' },
|
|
590
|
+
'fix-php-no-rate-limit': { fn: fixPhpNoRateLimit, tier: 2, title: 'Add throttle middleware to routes' },
|
|
591
|
+
'fix-php-no-input-filter': { fn: fixPhpNoInputFilter, tier: 2, title: 'Replace $_GET with filter_input()' },
|
|
592
|
+
'fix-php-weak-random': { fn: fixPhpWeakRandom, tier: 1, title: 'Replace rand() with random_int()' },
|
|
593
|
+
'fix-php-no-type-hints': { fn: fixPhpNoTypeHints, tier: 1, title: 'Add type hints to function parameters' },
|
|
594
|
+
'fix-php-no-csrf': { fn: fixPhpNoCsrf, tier: 2, title: 'Add CSRF token to POST forms' },
|
|
595
|
+
'fix-php-blade-unescaped': { fn: fixPhpBladUnescaped, tier: 1, title: 'Replace {!! !!} with {{ }} in Blade templates' },
|
|
596
|
+
'fix-php-laravel-raw-query': { fn: fixPhpLaravelRawQuery, tier: 2, title: 'Replace DB::raw with parameterized query' },
|
|
597
|
+
'fix-php-no-validation': { fn: fixPhpNoValidation, tier: 2, title: 'Add request validation to controller' },
|
|
598
|
+
'fix-php-env-exposure': { fn: fixPhpEnvExposure, tier: 1, title: 'Remove phpinfo() calls' },
|
|
599
|
+
'fix-php-debug-mode': { fn: fixPhpDebugMode, tier: 1, title: 'Set APP_DEBUG to false' },
|
|
600
|
+
'fix-php-open-basedir': { fn: fixPhpOpenBasedir, tier: 1, title: 'Set open_basedir restriction' },
|
|
601
|
+
'fix-php-allow-url-include': { fn: fixPhpAllowUrlInclude, tier: 1, title: 'Disable allow_url_include' },
|
|
602
|
+
'fix-php-no-error-display': { fn: fixPhpNoErrorDisplay, tier: 1, title: 'Disable display_errors' },
|
|
603
|
+
'fix-php-hardcoded-secret': { fn: fixPhpHardcodedSecret, tier: 1, title: 'Replace hardcoded secret with getenv()' },
|
|
604
|
+
'fix-php-insecure-upload': { fn: fixPhpInsecureUpload, tier: 2, title: 'Validate mime type and extension on upload' },
|
|
605
|
+
'fix-php-no-output-buffering': { fn: fixPhpNoOutputBuffering, tier: 1, title: 'Add output buffering with ob_start()' },
|
|
606
|
+
'fix-php-direct-sql': { fn: fixPhpDirectSql, tier: 2, title: 'Replace direct SQL with prepared statement' },
|
|
607
|
+
'fix-php-session-regenerate': { fn: fixPhpSessionRegenerate, tier: 2, title: 'Regenerate session ID on login' },
|
|
608
|
+
};
|