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)


199
200
201
# File 'app/models/wp_item.rb', line 199

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



208
209
210
# File 'app/models/wp_item.rb', line 208

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)


235
236
237
238
239
# File 'app/models/wp_item.rb', line 235

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)


245
246
247
248
249
# File 'app/models/wp_item.rb', line 245

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:



264
265
266
267
268
269
# File 'app/models/wp_item.rb', line 264

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)


75
76
77
78
# File 'app/models/wp_item.rb', line 75

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)


112
113
114
115
116
117
# File 'app/models/wp_item.rb', line 112

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)


135
136
137
138
139
140
# File 'app/models/wp_item.rb', line 135

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)


124
125
126
127
128
# File 'app/models/wp_item.rb', line 124

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.



103
104
105
# File 'app/models/wp_item.rb', line 103

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



81
82
83
84
# File 'app/models/wp_item.rb', line 81

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

#latest_versionString

Returns:

  • (String)


64
65
66
# File 'app/models/wp_item.rb', line 64

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

#outdated?Boolean

Returns:

  • (Boolean)


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

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



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

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)


175
176
177
# File 'app/models/wp_item.rb', line 175

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

#popular?Boolean

Not used anywhere ATM

Returns:

  • (Boolean)


70
71
72
# File 'app/models/wp_item.rb', line 70

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

#potential_readme_filenamesObject



227
228
229
# File 'app/models/wp_item.rb', line 227

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



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

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)


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

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.



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

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



203
204
205
# File 'app/models/wp_item.rb', line 203

def to_s
  slug
end

#url(path = nil) ⇒ String

Parameters:

  • path (String) (defaults to: nil)

    Optional path to merge with the uri

Returns:

  • (String)


191
192
193
194
195
196
# File 'app/models/wp_item.rb', line 191

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
# 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)
    @vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
  end

  @vulnerabilities
end

#vulnerable_to?(vuln) ⇒ Boolean

Checks if the wp_item is vulnerable to a specific vulnerability

Parameters:

  • vuln (Vulnerability)

    Vulnerability to check the item against

Returns:

  • (Boolean)


55
56
57
58
59
60
61
# File 'app/models/wp_item.rb', line 55

def vulnerable_to?(vuln)
  return false if version && vuln&.introduced_in && version < vuln.introduced_in

  return true unless version && vuln&.fixed_in && !vuln.fixed_in.empty?

  version < vuln.fixed_in
end