Class: WPScan::Model::WpItem

Inherits:
Object
  • Object
show all
Includes:
Finders::Finding, WordpressOrgData, Target::Platform::PHP, Target::Server::Generic, Vulnerable
Defined in:
app/models/wp_item.rb,
app/models/wp_item/wordpress_org_data.rb

Overview

WpItem (superclass of Plugin & Theme)

Direct Known Subclasses

Plugin, Theme

Defined Under Namespace

Modules: WordpressOrgData

Constant Summary collapse

READMES =

Most common readme filenames, based on checking all public plugins and themes.

%w[readme.txt README.txt README.md readme.md Readme.txt].freeze

Constants included from WordpressOrgData

WordpressOrgData::WORDPRESS_ORG_API_TIMEOUT

Constants included from Target::Platform::PHP

Target::Platform::PHP::DEBUG_LOG_PATTERN, Target::Platform::PHP::ERROR_LOG_PATTERN, Target::Platform::PHP::FPD_PATTERN

Constants included from Finders::Finding

Finders::Finding::FINDING_OPTS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from WordpressOrgData

#active_installs, #wordpress_org_api_url, #wordpress_org_data

Methods included from Target::Server::Generic

#directory_listing_entries, #headers, #server

Methods included from Target::Platform::PHP

#debug_log?, #full_path_disclosure?, #full_path_disclosure_entries, #install_body_cap, #log_file?, #stream_capped_body

Methods included from Finders::Finding

#<=>, #confidence, #confidence=, #confirmed_by, #eql?, included, #interesting_entries, #parse_finding_options

Methods included from Vulnerable

#filtered_vulnerabilities, #vulnerability_filter, #vulnerable?

Constructor Details

#initialize(slug, blog, opts = {}) ⇒ WpItem

Returns a new instance of WpItem.

Parameters:

  • slug (String)

    The plugin/theme slug

  • blog (Target)

    The targeted blog

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :mode (Symbol)

    The detection mode to use

  • :version_detection (Hash)

    The options to use when looking for the version

  • :url (String)

    The URL of the item



25
26
27
28
29
30
31
32
33
34
# File 'app/models/wp_item.rb', line 25

def initialize(slug, blog, opts = {})
  @slug  = Addressable::URI.unencode(slug)
  @blog  = blog
  @uri   = Addressable::URI.parse(opts[:url]) if opts[:url]

  @detection_opts         = { mode: opts[:mode] }
  @version_detection_opts = opts[:version_detection] || {}

  parse_finding_options(opts)
end

Instance Attribute Details

#blogObject (readonly)

Returns the value of attribute blog.



15
16
17
# File 'app/models/wp_item.rb', line 15

def blog
  @blog
end

#db_dataObject (readonly)

Returns the value of attribute db_data.



15
16
17
# File 'app/models/wp_item.rb', line 15

def db_data
  @db_data
end

#detection_optsObject (readonly)

Returns the value of attribute detection_opts.



15
16
17
# File 'app/models/wp_item.rb', line 15

def detection_opts
  @detection_opts
end

#path_from_blogObject (readonly)

Returns the value of attribute path_from_blog.



15
16
17
# File 'app/models/wp_item.rb', line 15

def path_from_blog
  @path_from_blog
end

#slugObject (readonly)

Returns the value of attribute slug.



15
16
17
# File 'app/models/wp_item.rb', line 15

def slug
  @slug
end

#uriObject (readonly)

Returns the value of attribute uri.



15
16
17
# File 'app/models/wp_item.rb', line 15

def uri
  @uri
end

#version_detection_optsObject (readonly)

Returns the value of attribute version_detection_opts.



15
16
17
# File 'app/models/wp_item.rb', line 15

def version_detection_opts
  @version_detection_opts
end

Instance Method Details

#==(other) ⇒ Boolean

Returns:

  • (Boolean)


201
202
203
# File 'app/models/wp_item.rb', line 201

def ==(other)
  self.class == other.class && slug == other.slug
end

#classifySymbol

Returns The Class symbol associated to the item.

Returns:

  • (Symbol)

    The Class symbol associated to the item



210
211
212
# File 'app/models/wp_item.rb', line 210

def classify
  @classify ||= classify_slug(slug)
end

#directory_listing?(path = nil, params = {}) ⇒ Boolean

Parameters:

  • path (String) (defaults to: nil)
  • params (Hash) (defaults to: {})

    The request params

Returns:

  • (Boolean)


237
238
239
240
241
# File 'app/models/wp_item.rb', line 237

def directory_listing?(path = nil, params = {})
  return false if detection_opts[:mode] == :passive

  super
end

#error_log?(path = 'error_log', params = {}) ⇒ Boolean

Parameters:

  • path (String) (defaults to: 'error_log')
  • params (Hash) (defaults to: {})

    The request params

Returns:

  • (Boolean)


247
248
249
250
251
# File 'app/models/wp_item.rb', line 247

def error_log?(path = 'error_log', params = {})
  return false if detection_opts[:mode] == :passive

  super
end

#head_and_get(path, codes = [200], params = {}) ⇒ Typhoeus::Response

See WPScan::Target#head_and_get

This is used by the error_log? above in the super() to have the correct path (ie readme.txt checked from the plugin/theme location and not from the blog root). Could also be used in finders

Parameters:

  • path (String)
  • codes (Array<String>) (defaults to: [200])
  • params (Hash) (defaults to: {})

    The requests params

Options Hash (params):

  • :head (Hash)

    Request params for the HEAD

  • :get (hash)

    Request params for the GET

Returns:



266
267
268
269
270
271
# File 'app/models/wp_item.rb', line 266

def head_and_get(path, codes = [200], params = {})
  final_path = @path_from_blog.dup # @path_from_blog is set in the plugin/theme
  final_path << path unless path.nil?

  blog.head_and_get(final_path, codes, params)
end

#last_updatedString?

Returns:

  • (String, nil)


77
78
79
80
# File 'app/models/wp_item.rb', line 77

def last_updated
  resolve_last_updated unless defined?(@last_updated)
  @last_updated
end

#last_updated_cli_suffixString

Parenthesized annotation appended to the CLI "Last Updated" line, e.g. " (3 months ago, per wordpress.org)". Empty string when there is nothing to annotate.

Returns:

  • (String)


114
115
116
117
118
119
# File 'app/models/wp_item.rb', line 114

def last_updated_cli_suffix
  parts = []
  parts << last_updated_relative if last_updated_relative
  parts << 'per WordPress.org' if last_updated_source == 'WordPress.org'
  parts.empty? ? '' : " (#{parts.join(', ')})"
end

#last_updated_displayString?

Friendly representation of last_updated, used in CLI output, matching the wordpress.org API style (e.g. "2026-04-14 12:01pm GMT"). Falls back to the raw string when the value cannot be parsed.

Returns:

  • (String, nil)


137
138
139
140
141
142
# File 'app/models/wp_item.rb', line 137

def last_updated_display
  return @last_updated_display if defined?(@last_updated_display)

  time = parse_last_updated
  @last_updated_display = time ? time.strftime('%Y-%m-%d %-l:%M%P GMT') : last_updated
end

#last_updated_isoString?

ISO 8601 (UTC) representation of last_updated, used by the JSON output so downstream consumers always see a consistent format regardless of whether the value came from the local DB or the wordpress.org API.

Returns:

  • (String, nil)


126
127
128
129
130
# File 'app/models/wp_item.rb', line 126

def last_updated_iso
  return @last_updated_iso if defined?(@last_updated_iso)

  @last_updated_iso = parse_last_updated&.iso8601
end

#last_updated_relativeString?

Returns Human-friendly relative time hint for last_updated (e.g. "3 months ago"). nil when last_updated cannot be parsed.

Returns:

  • (String, nil)

    Human-friendly relative time hint for last_updated (e.g. "3 months ago"). nil when last_updated cannot be parsed.



105
106
107
# File 'app/models/wp_item.rb', line 105

def last_updated_relative
  @last_updated_relative ||= relative_time_for(last_updated)
end

#last_updated_sourceString?

Returns 'db', 'wordpress.org', or nil when last_updated is unknown.

Returns:

  • (String, nil)

    'db', 'wordpress.org', or nil when last_updated is unknown



83
84
85
86
# File 'app/models/wp_item.rb', line 83

def last_updated_source
  resolve_last_updated unless defined?(@last_updated_source)
  @last_updated_source
end

#latest_versionString

Returns:

  • (String)


66
67
68
# File 'app/models/wp_item.rb', line 66

def latest_version
  @latest_version ||= ['latest_version'] ? Model::Version.new(['latest_version']) : nil
end

#outdated?Boolean

Returns:

  • (Boolean)


182
183
184
185
186
187
188
# File 'app/models/wp_item.rb', line 182

def outdated?
  @outdated ||= if version && latest_version
                  version < latest_version
                else
                  false
                end
end

#parse_last_updatedTime?

Returns last_updated parsed as UTC Time, or nil.

Returns:

  • (Time, nil)

    last_updated parsed as UTC Time, or nil



145
146
147
148
149
150
151
152
# File 'app/models/wp_item.rb', line 145

def parse_last_updated
  value = last_updated
  return nil if value.nil? || value.to_s.strip.empty?

  Time.parse(value.to_s).utc
rescue ArgumentError, TypeError
  nil
end

#pluralize_unit(count, unit) ⇒ String

Returns:

  • (String)


177
178
179
# File 'app/models/wp_item.rb', line 177

def pluralize_unit(count, unit)
  "#{count} #{unit}#{'s' if count != 1} ago"
end

#popular?Boolean

Not used anywhere ATM

Returns:

  • (Boolean)


72
73
74
# File 'app/models/wp_item.rb', line 72

def popular?
  @popular ||= ['popular'] ? true : false
end

#potential_readme_filenamesObject



229
230
231
# File 'app/models/wp_item.rb', line 229

def potential_readme_filenames
  @potential_readme_filenames ||= READMES
end

#readme_urlString, False

Returns The readme url if found, false otherwise.

Returns:

  • (String, False)

    The readme url if found, false otherwise



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/models/wp_item.rb', line 215

def readme_url
  return if detection_opts[:mode] == :passive

  return @readme_url unless @readme_url.nil?

  potential_readme_filenames.each do |path|
    t_url = url(path)

    return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200
  end

  @readme_url = false
end

#relative_time_for(value) ⇒ String?

Parameters:

  • value (String, nil)

    A timestamp parseable by Time.parse

Returns:

  • (String, nil)


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'app/models/wp_item.rb', line 156

def relative_time_for(value)
  return nil if value.nil? || value.to_s.strip.empty?

  time  = Time.parse(value.to_s).utc
  delta = Time.now.utc - time
  return 'in the future' if delta.negative?

  seconds = delta.to_i
  case seconds
  when 0...60        then 'just now'
  when 60...3600     then pluralize_unit(seconds / 60, 'minute')
  when 3600...86_400 then pluralize_unit(seconds / 3600, 'hour')
  when 86_400...2_592_000 then pluralize_unit(seconds / 86_400, 'day')
  when 2_592_000...31_536_000 then pluralize_unit(seconds / 2_592_000, 'month')
  else pluralize_unit(seconds / 31_536_000, 'year')
  end
rescue ArgumentError, TypeError
  nil
end

#resolve_last_updatedObject

WordPress.org takes precedence over local DB metadata since the WPScan DB is not synced in real time and may be stale.



90
91
92
93
94
95
96
97
98
99
100
101
# File 'app/models/wp_item.rb', line 90

def resolve_last_updated
  if (api_value = wordpress_org_data['last_updated'])
    @last_updated        = api_value
    @last_updated_source = 'WordPress.org'
  elsif (db_value = ['last_updated'])
    @last_updated        = db_value
    @last_updated_source = 'db'
  else
    @last_updated        = nil
    @last_updated_source = nil
  end
end

#to_sObject



205
206
207
# File 'app/models/wp_item.rb', line 205

def to_s
  slug
end

#url(path = nil) ⇒ String

Parameters:

  • path (String) (defaults to: nil)

    Optional path to merge with the uri

Returns:

  • (String)


193
194
195
196
197
198
# File 'app/models/wp_item.rb', line 193

def url(path = nil)
  return unless @uri
  return @uri.to_s unless path

  @uri.join(Addressable::URI.encode(path)).to_s
end

#vulnerabilitiesArray<Vulnerabily>

Returns:

  • (Array<Vulnerabily>)


37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'app/models/wp_item.rb', line 37

def vulnerabilities
  return @vulnerabilities if @vulnerabilities

  @vulnerabilities = []

  Array(db_data['vulnerabilities']).each do |json_vuln|
    vulnerability = Vulnerability.load_from_json(json_vuln)
    vulnerability.detected_version = version || nil
    @vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
  end

  @vulnerabilities
end

#vulnerable_to?(vuln) ⇒ Boolean

Checks if the wp_item is vulnerable to a specific vulnerability. A vuln with multiple backported ranges matches when any single range covers the detected version.

Parameters:

  • vuln (Vulnerability)

    Vulnerability to check the item against

Returns:

  • (Boolean)


58
59
60
61
62
63
# File 'app/models/wp_item.rb', line 58

def vulnerable_to?(vuln)
  return true unless version
  return true if vuln.nil?

  vuln.affects?(version)
end