Testing Dynamic Plugin Version Detection in WPScan
This test suite validates the dynamic version detection capabilities for WordPress plugins in WPScan. It implements comprehensive testing for various version finder configurations and ensures accurate plugin version identification through both passive and aggressive scanning methods.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
wpscanteam/wpscan
spec/lib/finders/dynamic_finder/plugin_version_spec.rb
# frozen_string_literal: true
# All Plugin Dynamic Finders returning a Version are tested here.
# When adding one to the spec/fixtures/db/dynamic_finder.yml, a few files have
# to be edited/created
#
# - spec/fixtures/dynamic_finder/expected.yml with the expected result/s
# - Then, depending on the finder class used: spec/fixtures/dynamic_finder/plugin_version/
#
# Furthermore, the fixtures files _passive_all.html are also used by plugins/themes
# finders in spec/app/finders/plugins|themes to check the items existence from the homepage
#
# In case of a failure, it's recommended to use rspec -e "<Full Description>" while fixing.
# e.g: rspec -e "WPScan::Finders::PluginVersion::Cardealerpress::HeaderPattern#passive"
# The -e option can also be used to test all HeaderPattern, for example: rspec -e "::HeaderPattern"
expected_all = df_expected_all['plugins']
WPScan::DB::DynamicFinders::Plugin.versions_finders_configs.each do |slug, configs|
WPScan::DB::DynamicFinders::Plugin.create_versions_finders(slug)
configs.each do |finder_class, config|
finder_super_class = config['class'] || finder_class
# The QueryParameter specs are slow given the huge fixture file
# If someone find a fix for that, please share!
describe df_tested_class_constant('PluginVersion', finder_class, slug), slow: true do
subject(:finder) { described_class.new(plugin) }
let(:plugin) { WPScan::Model::Plugin.new(slug, target) }
let(:target) { WPScan::Target.new('http://wp.lab/') }
let(:fixtures) { DYNAMIC_FINDERS_FIXTURES.join('plugin_version') }
let(:expected) do
if expected_all[slug][finder_class].is_a?(Hash)
[expected_all[slug][finder_class]]
else
expected_all[slug][finder_class]
end
end
before { allow(target).to receive(:content_dir).and_return('wp-content') }
describe '#passive', slow: true do
before do
if defined?(stubbed_homepage_res)
stub_request(:get, target.url).to_return(stubbed_homepage_res)
else
stub_request(:get, target.url)
end
if defined?(stubbed_404_res)
stub_request(:get, ERROR_404_URL_PATTERN).to_return(stubbed_404_res)
else
stub_request(:get, ERROR_404_URL_PATTERN)
end
end
if config['path']
context 'when PATH' do
it 'returns nil' do
expect(finder.passive).to eql nil
end
end
else
context 'when no PATH' do
context 'when the version is detected' do
context 'from the homepage' do
let(:ie_url) { target.url }
let(:stubbed_homepage_res) do
df_stubbed_response(
fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
finder_super_class
)
end
it 'returns the expected version/s' do
found = Array(finder.passive)
expect(found).to_not be_empty
found.each_with_index do |version, index|
expected_version = expected.at(index)
expected_ie = expected_version['interesting_entries'].map do |ie|
ie.gsub("#{target.url},", "#{ie_url},")
end
expect(version).to be_a WPScan::Model::Version
expect(version.number).to eql expected_version['number'].to_s
expect(version.found_by).to eql expected_version['found_by']
expect(version.interesting_entries).to match_array expected_ie
expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
end
end
end
context 'from the 404' do
let(:ie_url) { target.error_404_url }
let(:stubbed_404_res) do
df_stubbed_response(
fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
finder_super_class
)
end
it 'returns the expected version/s' do
found = Array(finder.passive)
expect(found).to_not be_empty
found.each_with_index do |version, index|
expected_version = expected.at(index)
expected_ie = expected_version['interesting_entries'].map do |ie|
ie.gsub("#{target.url},", "#{ie_url},")
end
expect(version).to be_a WPScan::Model::Version
expect(version.number).to eql expected_version['number'].to_s
expect(version.found_by).to eql expected_version['found_by']
expect(version.interesting_entries).to match_array expected_ie
expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
end
end
end
end
context 'when the version is not detected' do
it 'returns nil or an empty array' do
expect(finder.passive).to eql finder_super_class == 'QueryParameter' ? [] : nil
end
end
end
end
end
describe '#aggressive' do
let(:fixtures) { super().join(slug, finder_class.underscore) }
let(:stubbed_response) { { body: 'aa' } }
before do
stub_request(:get, plugin.url(config['path'])).to_return(stubbed_response) if config['path']
end
if config['path']
context 'when the version is detected' do
let(:stubbed_response) do
df_stubbed_response(fixtures.join(config['path']), finder_super_class)
end
it 'returns the expected version' do
found = Array(finder.aggressive)
expect(found).to_not be_empty
found.each_with_index do |version, index|
expected_version = expected.at(index)
expect(version).to be_a WPScan::Model::Version
expect(version.number).to eql expected_version['number'].to_s
expect(version.found_by).to eql expected_version['found_by']
expect(version.interesting_entries).to match_array expected_version['interesting_entries']
expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
end
end
end
context 'when the version is not detected' do
it 'returns nil or an empty array' do
expect(finder.aggressive).to eql finder_super_class == 'QueryParameter' ? [] : nil
end
end
else
it 'returns nil' do
expect(finder.aggressive).to eql nil
end
end
end
end
end
end