pluginscan 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.gitlab-ci.yml +16 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +46 -0
  6. data/.rubocop_todo.yml +36 -0
  7. data/CHANGELOG.md +89 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.lock +90 -0
  10. data/README.md +56 -0
  11. data/Rakefile +2 -0
  12. data/TODO.md +8 -0
  13. data/bin/pluginscan +53 -0
  14. data/lib/file_creator.rb +18 -0
  15. data/lib/pluginscan.rb +69 -0
  16. data/lib/pluginscan/error.rb +9 -0
  17. data/lib/pluginscan/error_printer.rb +17 -0
  18. data/lib/pluginscan/file_finder.rb +42 -0
  19. data/lib/pluginscan/printer.rb +14 -0
  20. data/lib/pluginscan/reports/cloc_report.rb +27 -0
  21. data/lib/pluginscan/reports/cloc_report/cloc.rb +21 -0
  22. data/lib/pluginscan/reports/cloc_report/cloc_printer.rb +42 -0
  23. data/lib/pluginscan/reports/cloc_report/cloc_scanner.rb +41 -0
  24. data/lib/pluginscan/reports/cloc_report/system_cloc.rb +33 -0
  25. data/lib/pluginscan/reports/issues_report.rb +24 -0
  26. data/lib/pluginscan/reports/issues_report/error_list_printer.rb +99 -0
  27. data/lib/pluginscan/reports/issues_report/issue_checks.rb +382 -0
  28. data/lib/pluginscan/reports/issues_report/issue_checks/check.rb +55 -0
  29. data/lib/pluginscan/reports/issues_report/issue_checks/comment_checker.rb +13 -0
  30. data/lib/pluginscan/reports/issues_report/issue_checks/function_check.rb +32 -0
  31. data/lib/pluginscan/reports/issues_report/issue_checks/variable_check.rb +14 -0
  32. data/lib/pluginscan/reports/issues_report/issue_checks/variable_safety_checker.rb +112 -0
  33. data/lib/pluginscan/reports/issues_report/issues_models/check_findings.rb +29 -0
  34. data/lib/pluginscan/reports/issues_report/issues_models/issues.rb +31 -0
  35. data/lib/pluginscan/reports/issues_report/issues_printer.rb +34 -0
  36. data/lib/pluginscan/reports/issues_report/issues_printer/check_findings_printer.rb +37 -0
  37. data/lib/pluginscan/reports/issues_report/issues_printer/file_issues_printer.rb +36 -0
  38. data/lib/pluginscan/reports/issues_report/issues_printer/finding_printer.rb +38 -0
  39. data/lib/pluginscan/reports/issues_report/issues_printer_factory.rb +19 -0
  40. data/lib/pluginscan/reports/issues_report/issues_scanner.rb +49 -0
  41. data/lib/pluginscan/reports/issues_report/issues_scanner/file_issues_scanner.rb +39 -0
  42. data/lib/pluginscan/reports/issues_report/issues_scanner/line_issues_scanner.rb +15 -0
  43. data/lib/pluginscan/reports/issues_report/issues_scanner/utf8_checker.rb +14 -0
  44. data/lib/pluginscan/reports/sloccount_report.rb +26 -0
  45. data/lib/pluginscan/reports/sloccount_report/sloccount.rb +19 -0
  46. data/lib/pluginscan/reports/sloccount_report/sloccount_printer.rb +22 -0
  47. data/lib/pluginscan/reports/sloccount_report/sloccount_scanner.rb +86 -0
  48. data/lib/pluginscan/reports/vulnerability_report.rb +28 -0
  49. data/lib/pluginscan/reports/vulnerability_report/advisories_api.rb +23 -0
  50. data/lib/pluginscan/reports/vulnerability_report/vulnerabilities_printer.rb +55 -0
  51. data/lib/pluginscan/reports/vulnerability_report/vulnerability_scanner.rb +17 -0
  52. data/lib/pluginscan/reports/vulnerability_report/wp_vuln_db_api.rb +77 -0
  53. data/lib/pluginscan/version.rb +3 -0
  54. data/pluginscan.gemspec +31 -0
  55. data/spec/acceptance/cloc_spec.rb +54 -0
  56. data/spec/acceptance/create_error_list_file_spec.rb +29 -0
  57. data/spec/acceptance/issues_spec.rb +197 -0
  58. data/spec/acceptance/pluginscan_spec.rb +18 -0
  59. data/spec/acceptance/sloccount_spec.rb +39 -0
  60. data/spec/acceptance/vulnerabilities_spec.rb +57 -0
  61. data/spec/acceptance_spec_helper.rb +10 -0
  62. data/spec/checks_examples_spec.rb +352 -0
  63. data/spec/file_creator_spec.rb +51 -0
  64. data/spec/pluginscan/cloc_scanner/cloc_scanner_spec.rb +64 -0
  65. data/spec/pluginscan/cloc_scanner/cloc_spec.rb +30 -0
  66. data/spec/pluginscan/file_finder_spec.rb +91 -0
  67. data/spec/pluginscan/issues_scanner/check_findings_spec.rb +22 -0
  68. data/spec/pluginscan/issues_scanner/error_list_printer_ignores_spec.rb +35 -0
  69. data/spec/pluginscan/issues_scanner/error_list_printer_spec.rb +42 -0
  70. data/spec/pluginscan/issues_scanner/file_issues_scanner_spec.rb +25 -0
  71. data/spec/pluginscan/issues_scanner/issues_printer_factory_spec.rb +9 -0
  72. data/spec/pluginscan/issues_scanner/issues_spec.rb +55 -0
  73. data/spec/pluginscan/issues_scanner/variable_check_spec.rb +13 -0
  74. data/spec/pluginscan/issues_scanner/variable_safety_checker_spec.rb +81 -0
  75. data/spec/pluginscan/issues_scanner_spec.rb +21 -0
  76. data/spec/pluginscan/sloccount_scanner/sloccount_scanner_spec.rb +95 -0
  77. data/spec/pluginscan/sloccount_scanner/sloccount_spec.rb +72 -0
  78. data/spec/pluginscan/vulnerability_scanner_spec.rb +96 -0
  79. data/spec/process_spec_helper.rb +6 -0
  80. data/spec/spec_helper.rb +70 -0
  81. data/spec/support/acceptance_helpers.rb +68 -0
  82. data/spec/support/file_helpers.rb +35 -0
  83. data/spec/support/heredoc_helper.rb +7 -0
  84. data/spec/support/process_helpers.rb +25 -0
  85. data/spec/support/shared_examples_for_issue_checks.rb +31 -0
  86. data/spec/support/vcr_helper.rb +6 -0
  87. data/vcr_cassettes/wpvulndb/relevanssi.yml +78 -0
  88. metadata +342 -0
@@ -0,0 +1,382 @@
1
+ require "pluginscan/reports/issues_report/issue_checks/check"
2
+ require "pluginscan/reports/issues_report/issue_checks/function_check"
3
+ require "pluginscan/reports/issues_report/issue_checks/variable_check"
4
+ require "pluginscan/reports/issues_report/issue_checks/variable_safety_checker"
5
+ require "pluginscan/reports/issues_report/issue_checks/comment_checker"
6
+
7
+ module Pluginscan
8
+ THE_CHECKS = [
9
+ #
10
+ # Look for superglobal arrays. Use of data from these arrays should be checked for unsanitised or unescaped use.
11
+ #
12
+ VariableCheck.new(
13
+ name: 'Superglobal',
14
+ message: 'Superglobal requires manual review',
15
+ variables: %w(
16
+ $_GET
17
+ $_POST
18
+ $_SERVER
19
+ $_REQUEST
20
+ $_COOKIE
21
+ $_ENV
22
+ $_FILES
23
+ ),
24
+ ),
25
+
26
+ #
27
+ # Check for SQLi
28
+ #
29
+ Check.new(
30
+ name: 'Database access',
31
+ message: 'Database access requires manual review',
32
+ patterns: [
33
+ /\$wpdb/,
34
+ ],
35
+ ignores: [
36
+ /global.+\$wpdb/,
37
+ lambda do |line|
38
+ # Try to exclude some things that are definitely not injectable
39
+ # safe_things = %w(prefix postmeta comments commentmeta links options posts terms term_relationships term_taxonomy usermeta users blogs blog_versions registration_log signups site sitecategories sitemeta).each do |safe|
40
+ # line.gsub!(/\$wpdb->#{safe}/, '')
41
+ # end
42
+
43
+ # If it doesn't look like the whole call is on this line, all bets are off
44
+ return false if line.count('(') != line.count(')')
45
+
46
+ # If it looks like they might be concatenating a string, all bets are off
47
+ return false if line.match(/["']{1}\s*\./) || line.match(/\.\s['"]{1}/)
48
+
49
+ # If the first occurrence of $wpdb is a function call ($wpdb->thing())
50
+ # then we don't care about it or anything before it
51
+ line = line.gsub(/^.*?\$wpdb-/, '')
52
+
53
+ # If there's anything before the first wpdb, which can't result in it getting
54
+ # assigned to something else, then we can delete all of that
55
+ line = line.gsub(/^[^(=]*?\$wpdb/, '')
56
+
57
+ # $wpdb->whatever is safe
58
+ line = line.gsub(Regexp.new(Regexp.escape("$wpdb->")), '')
59
+
60
+ # Are there any variables left?
61
+ !line.match(/\$[a-z]{1}[a-z0-9_]*/)
62
+ end,
63
+ ]
64
+ ),
65
+
66
+ FunctionCheck.new(
67
+ name: 'MySQL functions',
68
+ message: 'People shouldn\'t use MySQL functions in WordPress.',
69
+ patterns: [
70
+ /mysqli?_[a-z0-9_]+/,
71
+ ]
72
+ ),
73
+
74
+ #
75
+ # Arbitrary code execution
76
+ #
77
+ FunctionCheck.new(
78
+ name: 'PHP code generation',
79
+ message: 'Code generation functions might be bad',
80
+ function_names: %w(
81
+ eval
82
+ create_function
83
+ assert
84
+ ),
85
+ ),
86
+
87
+ FunctionCheck.new(
88
+ name: 'User-controllable function calls',
89
+ message: 'What functions are they calling?',
90
+ patterns: [
91
+ /\$\$[a-zA-Z0-9_]+\s*\(/,
92
+ ],
93
+ function_names: %w(
94
+ call_user_func
95
+ call_user_func_array
96
+ ),
97
+ ),
98
+
99
+ #
100
+ # Execution of system commands
101
+ #
102
+ FunctionCheck.new(
103
+ name: 'System calls',
104
+ message: 'Executing system commands might be bad',
105
+ patterns: [
106
+ /`.+`/,
107
+ ],
108
+ function_names: %w(
109
+ popen
110
+ expect_popen
111
+ proc_optioni
112
+ exec
113
+ shell_exec
114
+ system
115
+ passthru
116
+ ),
117
+ ),
118
+
119
+ #
120
+ # File manipulation: arbitrary code, altering system configs, info leakage
121
+ #
122
+ FunctionCheck.new(
123
+ name: 'File operations',
124
+ message: 'File operations require manual review',
125
+ patterns: [
126
+ /xdiff_[a-z0-9_]+/,
127
+ ],
128
+ function_names: %w(
129
+ bzwrite
130
+ chmod
131
+ chgrp
132
+ chown
133
+ copy
134
+ file_put_contents
135
+ fputscv
136
+ fputs
137
+ fprintf
138
+ ftruncate
139
+ fwrite
140
+ gzwrite
141
+ gzputs
142
+ loadXML
143
+ makedir
144
+ move_uploaded_file
145
+ rename
146
+ unlink
147
+ vfprintf
148
+ yaml_emit_file
149
+ fread
150
+ fget
151
+ fgets
152
+ fgetss
153
+ fgetc
154
+ fpassthru
155
+ glob
156
+ file_get_contents
157
+ fgetcsv
158
+ file
159
+ bzread
160
+ gzread
161
+ gzgets
162
+ gzgetss
163
+ gzgetc
164
+ gzpassthru
165
+ finfo_file
166
+ highlight_file
167
+ show_source
168
+ readlink
169
+ ),
170
+ ),
171
+
172
+ #
173
+ # Attempts to obfuscate badware
174
+ #
175
+ FunctionCheck.new(
176
+ name: 'Possible obfuscation',
177
+ message: 'Are these obfuscating code or other badstuff?',
178
+ function_names: %w(
179
+ str_rot13
180
+ uudecode
181
+ base64_decode
182
+ base64_encode
183
+ ),
184
+ ),
185
+
186
+ #
187
+ # Network activity - remote shells, information leakage
188
+ #
189
+ FunctionCheck.new(
190
+ name: 'Network functions',
191
+ message: 'Who\'s sending what and where?',
192
+ patterns: [
193
+ /ftp_[a-z0-9_]+/,
194
+ /socket_[a-z0-9_]+/,
195
+ ],
196
+ function_names: %w(
197
+ curl_exec
198
+ curl_setopt
199
+ fsockopen
200
+ get_headers
201
+ wp_get_http
202
+ wp_get_http_headers
203
+ wp_remote_get
204
+ wp_remote_post
205
+ wp_remote_request
206
+ wp_remote_head
207
+ ),
208
+ ),
209
+
210
+ #
211
+ # Potentially unsafe wordpress functions
212
+ #
213
+ FunctionCheck.new(
214
+ name: 'Potentially unsafe wordpress functions',
215
+ message: 'Some wordpress functions are not inherently safe, this functions return value may not be safe.',
216
+ function_names: %w(
217
+ wp_unslash
218
+ wp_get_referer
219
+ wp_get_original_referer
220
+ esc_sql
221
+ ),
222
+ ),
223
+
224
+ #
225
+ # PHP object injection
226
+ #
227
+ FunctionCheck.new(
228
+ name: 'PHP object injection',
229
+ message: "If an object can be created where this function does something dangerous, that's bad",
230
+ function_names: %w(
231
+ __wakeup
232
+ __destruct
233
+ __toString
234
+ unserialize
235
+ ),
236
+ ),
237
+
238
+ #
239
+ # These functions parse strings into variables
240
+ #
241
+ FunctionCheck.new(
242
+ name: 'Variable parsing',
243
+ message: 'These functions parse strings into varables. Could be used as part of an Arbitrary Code Execution',
244
+ function_names: %w(
245
+ parse_str
246
+ extract
247
+ ),
248
+ ),
249
+
250
+ #
251
+ # Using primitives where the WordPress API provides a better alternative
252
+ #
253
+ FunctionCheck.new(
254
+ name: 'Redundant functions',
255
+ message: 'People using these should probably be using the WordPress API instead',
256
+ function_names: %w(
257
+ htmlspecialchars
258
+ mail
259
+ ),
260
+ # Should we ignore user-created mail() functions?
261
+ # Or is the point of this test to check for those? -dgms
262
+ ),
263
+
264
+ #
265
+ # What it says on the tin. WTF. lolwat.
266
+ #
267
+ FunctionCheck.new(
268
+ name: 'Batshit weird',
269
+ message: 'What on earth is it doing?',
270
+ patterns: [
271
+ /runkit_[a-z0-9_]+/,
272
+ /ldap_[a-z0-9_]+/,
273
+ ],
274
+ function_names: %w(
275
+ ini_set
276
+ put_env
277
+ sleep
278
+
279
+ apache_set_env
280
+
281
+ session_decode
282
+ ),
283
+ ),
284
+
285
+ #
286
+ # Accidents
287
+ #
288
+ Check.new(
289
+ name: 'Accidents',
290
+ message: 'Did someone do a silly?',
291
+ patterns: [
292
+ /\btodo\b/i,
293
+ /\bfixme\b/i,
294
+ /\bfuck\b/i,
295
+ /\bshit\b/i,
296
+ /\bcrap\b/i,
297
+ /\bbroken\b/i,
298
+ /\bhack\b/i,
299
+ /\bxxx\b/i,
300
+ /\bugly\b/i,
301
+ /\bscary\b/i,
302
+ /\bwtf\b/i,
303
+ /\bsecurity\b/i,
304
+ /\bbodge\b/i,
305
+ /\bhardening\b/i,
306
+ ]
307
+ ),
308
+
309
+ #
310
+ # Security Policy
311
+ #
312
+ Check.new(
313
+ name: 'Inline JavaScript',
314
+ message: 'Is there inline javascript which would prevent us enforcing a content security policy?',
315
+ patterns: [
316
+ /<script/i,
317
+ ],
318
+ ignores: [
319
+ /<script[^>]*\ +src=/i # tags containing src don't include inline JS
320
+ ]
321
+ ),
322
+
323
+ Check.new(
324
+ name: 'Inline CSS',
325
+ message: 'Is there inline css which would prevent us enforcing a content security policy?',
326
+ patterns: [
327
+ /<style/i,
328
+ ],
329
+ ),
330
+
331
+ Check.new(
332
+ name: 'Event Attributes',
333
+ message: 'Are there HTML event attributes which can execute inline JavaScript and thus prevent us enforcing a content security policy?',
334
+ patterns: [
335
+ /<[^>]* (on[[:alpha:]]+)=/i, # All event attributes start with "on" e.g. "onclick"
336
+ ],
337
+ ),
338
+
339
+ #
340
+ # Direct Loading of files
341
+ #
342
+ Check.new(
343
+ name: 'Direct loading',
344
+ message: 'Is someone trying to make a file that can be included directly?',
345
+ patterns: [
346
+ /wp-load\.php/,
347
+ ],
348
+ ),
349
+
350
+ #
351
+ # Unreliable IP addresses
352
+ #
353
+ Check.new(
354
+ name: 'Unreliable IP',
355
+ message: 'Are they making unsafe assumptions about client IP addresses?',
356
+ patterns: [
357
+ /HTTP_X_FORWARDED_FOR/,
358
+ /HTTP_CLIENT_IP/,
359
+ /HTTP_X_CLUSTER_CLIENT_IP/,
360
+ /HTTP_X_FORWARDED/,
361
+ /HTTP_FORWARDED_FOR/,
362
+ /HTTP_CF_CONNECTING_IP/,
363
+ /HTTP_X_REAL_IP/,
364
+ /HTTP_FORWARDED/,
365
+ ]
366
+ ),
367
+
368
+ #
369
+ # Version leaks
370
+ #
371
+ FunctionCheck.new(
372
+ name: 'Version leaks',
373
+ message: 'Are they leaking secrets to the enemy?',
374
+ function_names: [
375
+ 'phpversion',
376
+ ],
377
+ patterns: [
378
+ /(?:get_)?bloginfo\(.*?version/,
379
+ ],
380
+ ),
381
+ ].freeze
382
+ end
@@ -0,0 +1,55 @@
1
+ module Pluginscan
2
+ # Responsible for checking a string against set of rexgexps and returning all matches
3
+ class Check
4
+ attr_reader :name, :message, :patterns, :ignores
5
+
6
+ def initialize(check_hash)
7
+ @name = check_hash[:name]
8
+ @message = check_hash[:message]
9
+ @patterns = Array(check_hash[:patterns])
10
+ @ignores = Array(check_hash[:ignores]).map do |ignore_thing|
11
+ IgnoreThing.new(ignore_thing)
12
+ end
13
+ end
14
+
15
+ def run(content)
16
+ pattern_matches(content).map{ |matchdata| match(matchdata) }
17
+ end
18
+
19
+ def ignore?(_match, content)
20
+ # `match` is not used in the default case, but is included
21
+ # so that classes inheriting from this one can use it when deciding
22
+ # whether or not to ignore content by overriding this function
23
+ ignores.any? { |ignore_thing| ignore_thing.ignore?(content) }
24
+ end
25
+
26
+ private
27
+
28
+ def pattern_matches(content)
29
+ @patterns.map { |pattern| content.match(pattern) }.compact
30
+ end
31
+
32
+ IgnoreThing = Struct.new(:ignore_thing) do
33
+ def ignore?(content)
34
+ case ignore_thing
35
+ when Regexp
36
+ content.match(ignore_thing)
37
+ when Proc
38
+ ignore_thing.call(content)
39
+ end
40
+ end
41
+ end
42
+
43
+ def match(matchdata)
44
+ # Select the string we were interested in from the matchdata
45
+ matchdata.to_a.last
46
+ # This will return what we want assuming that we have either:
47
+ # a single item from a simple regex:
48
+ # #<MatchData "$_POST">.to_a
49
+ # => ["$_POST"]
50
+ # a whole match and a specific match, from a function_list item:
51
+ # #<MatchData "@unlink(" 1:"unlink">.to_a
52
+ # => ["@unlink(", "unlink"]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ module Pluginscan
2
+ # Responsible for deciding whether usages of a variable in a string are in a comment
3
+ module CommentChecker
4
+ COMMENT_REGEXPS = [
5
+ %r{^\s*//}, # line comment
6
+ /^\s*\*/, # block comment
7
+ ].freeze
8
+
9
+ def self.commented?(content)
10
+ COMMENT_REGEXPS.any? { |regexp| content.match(regexp) }
11
+ end
12
+ end
13
+ end