├── _config.yml
├── .wvc.yml.sample
├── tests
├── bootstrap.php
├── data
│ └── vPlugins
│ │ └── custom-css-js
│ │ └── custom-css-js.php
├── SettingsTest.php
└── ManagerTest.php
├── .gitignore
├── assets
└── wvc_banner.png
├── wp-vulnerability-check
├── src
├── ArrayIterator.php
├── Writer
│ ├── Writer.php
│ ├── Readable.php
│ └── HTML.php
├── OutputColored.php
├── Output.php
├── Colors.php
├── Settings.php
└── Manager.php
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── workflows
│ ├── wospm.yml
│ └── php_version_check.yml
└── PULL_REQUEST_TEMPLATE
│ └── pull_request_template.md
├── CONTRIBUTING.md
├── phpunit.xml
├── SECURITY.md
├── composer.json
├── LICENSE
├── wp-vulnerability-check.php
├── CODE_OF_CONDUCT
└── README.md
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-hacker
--------------------------------------------------------------------------------
/.wvc.yml.sample:
--------------------------------------------------------------------------------
1 | path:
2 | token:
3 | exclude:
4 | output: NO
5 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | next();
9 | return $this->current();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report to help us improve
4 | title: "[BUG] - Title"
5 | labels: bug
6 | assignees: umutphp
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. ...
16 | 2. ...
17 | 3. ...
18 | 4. ...
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Welcome!
2 |
3 | If you are reading this document then you are interested in contributing to wp-vulnerability-check, and that's awesome!
4 |
5 | All contributions are welcome: use-cases, documentation, code, patches, bug reports, feature requests, etc. You do not need to be a programmer to speak up!
6 |
7 | ## Tests
8 |
9 | First, install the dependencies;
10 |
11 | ```
12 | composer install
13 |
14 | ```
15 |
16 | After, you can run `phpunit` to trigger the unittests;
17 | ```
18 | ./vendor/bin/phpunit
19 |
20 | ```
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | tests
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: umutphp
7 |
8 | ---
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Below is the list of supported versions.
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 0.2.2 | :white_check_mark: |
10 |
11 | ## Reporting a Vulnerability
12 |
13 | If you want report a basic vulnerability, you can just [create an issue](https://github.com/umutphp/wp-vulnerability-check/issues). Please check the list before creating the issue.
14 |
15 | If you think that the vulnerability is very serious, you can contact directly with [me](https://umuts.info/).
16 |
--------------------------------------------------------------------------------
/tests/data/vPlugins/custom-css-js/custom-css-js.php:
--------------------------------------------------------------------------------
1 | ?php
2 | /**
3 | * Plugin Name: Simple Custom CSS and JS
4 | * Plugin URI: https://wordpress.org/plugins/custom-css-js/
5 | * Description: Easily add Custom CSS or JS to your website with an awesome editor.
6 | * Version: 3.22
7 | * Author: SilkyPress.com
8 | * Author URI: https://www.silkypress.com
9 | * License: GPL2
10 | *
11 | * Text Domain: custom-css-js
12 | * Domain Path: /languages/
13 | *
14 | * WC requires at least: 2.3.0
15 | * WC tested up to: 3.5
16 | */
17 |
18 | if ( ! defined( 'ABSPATH' ) ) {
19 | exit; // Exit if accessed directly
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/wospm.yml:
--------------------------------------------------------------------------------
1 | name: WOSPM Checker
2 | on: [push]
3 |
4 | jobs:
5 | wospm_checker:
6 | runs-on: ubuntu-latest
7 | name: WOSPM Checker
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 | - name: WOSPM Checker Github Action
12 | uses: WOSPM/wospm-checker-github-action@v1
13 | - name: Upload HTML Report When Success
14 | uses: actions/upload-artifact@v1
15 | with:
16 | name: HTML Report
17 | path: wospm.html
18 | - name: Upload HTML Report When Failed
19 | uses: actions/upload-artifact@v1
20 | if: failure()
21 | with:
22 | name: HTML Report
23 | path: wospm.html
24 |
--------------------------------------------------------------------------------
/src/Writer/Writer.php:
--------------------------------------------------------------------------------
1 | =5.6",
13 | "ext-curl": "*",
14 | "ext-json": "*",
15 | "symfony/yaml": "*"
16 | },
17 | "require-dev": {
18 | "phpunit/phpunit": "^9.5"
19 | },
20 | "config": {
21 | "bin-dir": "vendor/bin/"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "UmutPHP\\WPVulnerabilityCheck\\": "src/"
26 | }
27 | },
28 | "bin": [
29 | "wp-vulnerability-check"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Umut Işık
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/OutputColored.php:
--------------------------------------------------------------------------------
1 | writer = $writer ?: new Writer\Readable;
15 | $this->color = new Colors();
16 | }
17 |
18 | public function ok()
19 | {
20 | $this->writer->write($this->color->getColoredString('.', 'green'));
21 | $this->progress();
22 | }
23 |
24 | public function error()
25 | {
26 | $this->writer->write($this->color->getColoredString('V', 'red'));
27 | $this->progress();
28 | }
29 |
30 | public function fail()
31 | {
32 | $this->writer->write($this->color->getColoredString('-', 'yellow'));
33 | $this->progress();
34 | }
35 |
36 | protected function progress()
37 | {
38 | if (++$this->checkedPlugins % $this->pluginsPerLine === 0) {
39 | if ($this->totalPluginCount != 0) { // !=
40 | $percent = round($this->checkedPlugins / $this->totalPluginCount * 100);
41 | $current = $this->stringWidth($this->checkedPlugins, strlen($this->totalPluginCount));
42 | $this->writeLine(
43 | $this->color->getColoredString(" $current/$this->totalPluginCount ($percent %)", "blue")
44 | );
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/SettingsTest.php:
--------------------------------------------------------------------------------
1 | settings = new WPVulnerabilityCheck\Settings();
13 | }
14 |
15 | public function testParserArgumentsInvalidArgument()
16 | {
17 | $arguments = array('wp-vulnerability-check', '--invalid');
18 | $this->expectException(\Exception::class);
19 | WPVulnerabilityCheck\Settings::parseArguments($arguments);
20 | }
21 |
22 | public function testParserArgumentsWithNoDash()
23 | {
24 | $arguments = array('wp-vulnerability-check', 'withnodash');
25 | $this->expectException(\Exception::class);
26 | WPVulnerabilityCheck\Settings::parseArguments($arguments);
27 | }
28 |
29 | public function testParserArguments()
30 | {
31 | $arguments = array(
32 | 'wp-vulnerability-check',
33 | '--path', 'path',
34 | '--token', 'token',
35 | '--exclude', 'exclude',
36 | '--output', 'output'
37 | );
38 | $settings = WPVulnerabilityCheck\Settings::parseArguments($arguments);
39 |
40 | $this->assertEquals($settings->path, 'path');
41 | $this->assertEquals($settings->token, 'token');
42 | $this->assertEquals($settings->excluded, array('exclude'));
43 | $this->assertEquals($settings->output, 'output');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Writer/Readable.php:
--------------------------------------------------------------------------------
1 | write($output);
13 | echo PHP_EOL;
14 | }
15 |
16 | /**
17 | * Echo line
18 | *
19 | * @param string $output
20 | */
21 | public function write($output = "") {
22 | echo $output;
23 | }
24 |
25 | /**
26 | * Echo result line
27 | *
28 | * @param string $plugin
29 | * @param array $result
30 | */
31 | public function writeResult($plugin, $result) {
32 | $this->writeLine($plugin);
33 | $j = 1;
34 | foreach ($result as $key => $vuln) {
35 | $this->writeLine(
36 | "\t" . $j . ") id: " . $vuln["id"] . ", " . $vuln["title"]
37 | );
38 | if ($vuln["fixed_in"]) {
39 | $this->writeLine("\t" . "Fixed in Version " . $vuln["fixed_in"]);
40 | } else {
41 | $this->writeLine("\t" . "No known fix");
42 | }
43 |
44 | $references = [];
45 | foreach ($vuln["references"] as $key1 => $values) {
46 | foreach ($values as $value) {
47 | if ($key1 == 'url') {
48 | $references[] = $value;
49 | } else {
50 | $references[] = "$key1: $value";
51 | }
52 | }
53 | }
54 | $this->writeLine(
55 | "\tReferences: " . implode(", ", $references)
56 | );
57 | $j++;
58 | $this->writeLine();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/.github/workflows/php_version_check.yml:
--------------------------------------------------------------------------------
1 | name: PHP Version Check
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | php-7-3:
7 | name: PHP 7.3
8 | runs-on: ubuntu-latest
9 | container:
10 | image: umutphp/php-docker-images-for-ci:7.3
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Build
14 | run: "composer install --no-interaction --prefer-source"
15 | - name: Test
16 | run: ./vendor/bin/phpunit
17 |
18 | php-7-4:
19 | name: PHP 7.4
20 | runs-on: ubuntu-latest
21 | container:
22 | image: umutphp/php-docker-images-for-ci:7.4
23 | steps:
24 | - uses: actions/checkout@v2
25 | - name: Build
26 | run: "composer install --no-interaction --prefer-source"
27 | - name: Test
28 | run: ./vendor/bin/phpunit
29 |
30 | php-8-0:
31 | name: PHP 8.0
32 | runs-on: ubuntu-latest
33 | container:
34 | image: umutphp/php-docker-images-for-ci:8.0
35 | steps:
36 | - uses: actions/checkout@v2
37 | - name: Build
38 | run: "composer install --no-interaction --prefer-source"
39 | - name: Test
40 | run: ./vendor/bin/phpunit
41 |
42 | php-8-1:
43 | name: PHP 8.1
44 | runs-on: ubuntu-latest
45 | container:
46 | image: umutphp/php-docker-images-for-ci:8.1
47 | steps:
48 | - uses: actions/checkout@v2
49 | - name: Build
50 | run: "composer install --no-interaction --prefer-source"
51 | - name: Test
52 | run: ./vendor/bin/phpunit
53 |
54 | php-8-2:
55 | name: PHP 8.2
56 | runs-on: ubuntu-latest
57 | container:
58 | image: umutphp/php-docker-images-for-ci:8.2
59 | steps:
60 | - uses: actions/checkout@v2
61 | - name: Build
62 | run: "composer install --no-interaction --prefer-source"
63 | - name: Test
64 | run: ./vendor/bin/phpunit
65 |
--------------------------------------------------------------------------------
/tests/ManagerTest.php:
--------------------------------------------------------------------------------
1 | manager = new WPVulnerabilityCheck\Manager();
13 | }
14 |
15 | public function testCheckVulnerabilityWithVersion()
16 | {
17 | $array = array(
18 | 'vulnerabilities' => array(
19 | array(
20 | 'fixed_in' => 1
21 | ),
22 | array(
23 | 'fixed_in' => 3
24 | )
25 | )
26 | );
27 |
28 | $result = $this->manager->checkVulnerabilityWithVersion($array, 2);
29 |
30 | $this->assertEquals(1, count($result['vulnerabilities']));
31 | $this->assertTrue(isset($result['vulnerabilities'][1]));
32 | $this->assertFalse(isset($result['vulnerabilities'][0]));
33 | }
34 |
35 | public function testIsComponent() {
36 | $settings = new WPVulnerabilityCheck\Settings();
37 | $manager = new WPVulnerabilityCheck\Manager($settings);
38 |
39 | $this->assertTrue($manager->isComponent('plugin.php'));
40 | $this->assertFalse($manager->isComponent('index.php'));
41 | $this->assertFalse($manager->isComponent('..'));
42 | $this->assertFalse($manager->isComponent('.'));
43 | }
44 |
45 | public function testIsComponentWithExcluded() {
46 | $settings = new WPVulnerabilityCheck\Settings();
47 | $settings->excluded = array("plugin1", "plugin2");
48 | $manager = new WPVulnerabilityCheck\Manager($settings);
49 |
50 | $this->assertTrue($manager->isComponent('plugin.php'));
51 | $this->assertFalse($manager->isComponent('index.php'));
52 | $this->assertFalse($manager->isComponent('plugin1'));
53 | $this->assertFalse($manager->isComponent('plugin2'));
54 | $this->assertFalse($manager->isComponent('..'));
55 | $this->assertFalse($manager->isComponent('.'));
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Output.php:
--------------------------------------------------------------------------------
1 | writer = $writer ?: new Writer\Readable;
26 | }
27 |
28 | public function ok()
29 | {
30 | $this->writer->write('.');
31 | $this->progress();
32 | }
33 |
34 | public function error()
35 | {
36 | $this->writer->write('V');
37 | $this->progress();
38 | }
39 |
40 | public function fail()
41 | {
42 | $this->writer->write('-');
43 | $this->progress();
44 | }
45 |
46 | /**
47 | * @param string|null $line
48 | */
49 | public function writeLine($line = null)
50 | {
51 | $this->writer->write($line . PHP_EOL);
52 | }
53 |
54 | /**
55 | * @param int $count
56 | */
57 | public function writeNewLine($count = 1)
58 | {
59 | $this->writer->write(str_repeat(PHP_EOL, $count));
60 | }
61 |
62 | /**
63 | * @param int $count
64 | */
65 | public function setTotalPluginCount($count)
66 | {
67 | $this->totalPluginCount = $count;
68 | }
69 |
70 | /**
71 | * @param string $plugin
72 | * @param array $result
73 | * @param bool $withCodeSnippet
74 | */
75 | public function writeResult(string $plugin, array $result)
76 | {
77 | $this->writer->writeResult($plugin, $result);
78 | }
79 |
80 | protected function progress()
81 | {
82 | if (++$this->checkedPlugins % $this->pluginsPerLine === 0) {
83 | if ($this->totalPluginCount != 0) { // !=
84 | $percent = round($this->checkedPlugins / $this->totalPluginCount * 100);
85 | $current = $this->stringWidth($this->checkedPlugins, strlen($this->totalPluginCount));
86 | $this->writeLine(" $current/$this->totalPluginCount ($percent %)");
87 | }
88 | }
89 | }
90 |
91 | /**
92 | * @param string $input
93 | * @param int $width
94 | * @return string
95 | */
96 | protected function stringWidth($input, $width = 3)
97 | {
98 | $multiplier = $width - strlen($input);
99 | return str_repeat(' ', $multiplier > 0 ? $multiplier : 0) . $input;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Colors.php:
--------------------------------------------------------------------------------
1 | foreground_colors['black'] = '0;30';
11 | $this->foreground_colors['dark_gray'] = '1;30';
12 | $this->foreground_colors['blue'] = '0;34';
13 | $this->foreground_colors['light_blue'] = '1;34';
14 | $this->foreground_colors['green'] = '0;32';
15 | $this->foreground_colors['light_green'] = '1;32';
16 | $this->foreground_colors['cyan'] = '0;36';
17 | $this->foreground_colors['light_cyan'] = '1;36';
18 | $this->foreground_colors['red'] = '0;31';
19 | $this->foreground_colors['light_red'] = '1;31';
20 | $this->foreground_colors['purple'] = '0;35';
21 | $this->foreground_colors['light_purple'] = '1;35';
22 | $this->foreground_colors['brown'] = '0;33';
23 | $this->foreground_colors['yellow'] = '1;33';
24 | $this->foreground_colors['light_gray'] = '0;37';
25 | $this->foreground_colors['white'] = '1;37';
26 |
27 | $this->background_colors['black'] = '40';
28 | $this->background_colors['red'] = '41';
29 | $this->background_colors['green'] = '42';
30 | $this->background_colors['yellow'] = '43';
31 | $this->background_colors['blue'] = '44';
32 | $this->background_colors['magenta'] = '45';
33 | $this->background_colors['cyan'] = '46';
34 | $this->background_colors['light_gray'] = '47';
35 | }
36 |
37 | /**
38 | * Returns colored string
39 | * @param string $string
40 | * @param string|null $foreground_color
41 | * @param string|null background_color
42 | */
43 | public function getColoredString($string, $foreground_color = null, $background_color = null) {
44 | $colored_string = "";
45 |
46 | // Check if given foreground color found
47 | if (isset($this->foreground_colors[$foreground_color])) {
48 | $colored_string .= "\033[" . $this->foreground_colors[$foreground_color] . "m";
49 | }
50 | // Check if given background color found
51 | if (isset($this->background_colors[$background_color])) {
52 | $colored_string .= "\033[" . $this->background_colors[$background_color] . "m";
53 | }
54 |
55 | // Concatanate string and coloring
56 | $colored_string .= $string . "\033[0m";
57 |
58 | return $colored_string;
59 | }
60 |
61 | /**
62 | * Returns all foreground color names
63 | * @return array
64 | */
65 | public function getForegroundColors() {
66 | return array_keys($this->foreground_colors);
67 | }
68 |
69 | /**
70 | * Returns all background color names
71 | * @return array
72 | */
73 | public function getBackgroundColors() {
74 | return array_keys($this->background_colors);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/wp-vulnerability-check.php:
--------------------------------------------------------------------------------
1 |
17 | Options:
18 | --config Full path for the YAML config file. A sample config
19 | file is .wvc.yml.sample in root folder. CLI arguments
20 | override the values in config file.
21 | --path Full path of your WordPress installation.
22 | --plugins-path Relative path of the plugin folder. It is optional.
23 | Please specify if you don't use default plugin folder.
24 | --mu-plugins-path Relative path of the mu plugin folder. It is optional.
25 | Please specify if you don't use default mu plugin folder.
26 | --themes-path Relative path of the theme folder. It is optional.
27 | Please specify if you don't use default theme folder.
28 | --token Token got from wpscan.com
29 | --exclude Exclude the plugins given in comma separated format.
30 | --output The format of output. Valid values JSON, READABLE, HTML,
31 | NO (Default).
32 | --no-colors Disable the console colors. It is enabled by default.
33 | --version Show version.
34 | --help Print this help.
35 | getMessage() . PHP_EOL);
91 | echo PHP_EOL;
92 | showOptions();
93 | die(FAILED);
94 | }
95 |
96 | try {
97 | $check = new WPVulnerabilityCheck\Manager($settings);
98 | $status = $check->check();
99 |
100 | die($status ? SUCCESS : WITH_ERRORS);
101 | } catch (\Exception $e) {
102 | fwrite(STDERR, $e->getMessage() . PHP_EOL);
103 | die(FAILED);
104 | }
105 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html)
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq)
77 |
--------------------------------------------------------------------------------
/src/Writer/HTML.php:
--------------------------------------------------------------------------------
1 | write($output);
14 | echo PHP_EOL;
15 | }
16 |
17 | /**
18 | * Echo line
19 | *
20 | * @param string $output
21 | */
22 | public function write($output = "") {
23 | echo $output;
24 | }
25 |
26 | /**
27 | * Echo result line
28 | *
29 | * @param string $plugin
30 | * @param array $result
31 | */
32 | public function writeResult($plugin, $result) {
33 | $this->output .= "
$plugin
" . '';
34 | $j = 1;
35 | foreach ($result as $key => $vuln) {
36 | $this->output .= '
';
37 | $this->output .= '
' . $vuln["title"] . ' ' . $vuln["id"] .
38 | ' ' . $vuln["vuln_type"] . '
';
39 | if ($vuln["fixed_in"]) {
40 | $this->output .= "
Fixed in Version " . $vuln["fixed_in"] . "
";
41 | } else {
42 | $this->output .= '
No known fix
';
43 | }
44 | $this->output .= "
References;
";
45 |
46 | foreach ($vuln["references"] as $key1 => $values) {
47 | foreach ($values as $key2 => $value) {
48 | if ($key1 == 'url') {
49 | $this->output .= 'Url:
' . $value . '';
50 | } else {
51 | $this->output .= "$key1: $value";
52 | }
53 | }
54 | }
55 | $this->output .= "";
56 | $j++;
57 | $this->output .= '
';
58 | }
59 |
60 | $this->output .= '
';
61 | }
62 |
63 | public function __destruct() {
64 | $this->writeLine();
65 | $this->writeLine();
66 | $this->writeLine("HTML report is generated: vulnerability_result.html");
67 | $this->writeLine();
68 | file_put_contents(
69 | "vulnerability_result.html",
70 | $this->fileHeader() .
71 | $this->fileBody($this->output) .
72 | $this->fileFooter()
73 | );
74 | }
75 |
76 | private function fileHeader() {
77 | return '
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | Vulnerability Details
86 |
87 |
88 |
89 | ';
90 | }
91 |
92 | private function fileBody($result) {
93 | return '
94 |
95 |
96 |
Vulnerability Details
97 |
List of plugins and vulnerabilities;
98 | ' .
99 | $result .
100 | '
101 |
102 |
103 | ';
104 | }
105 |
106 | private function fileFooter() {
107 | return '
108 |
109 | ';
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WordPress Vulnerability Check (wp-vulnerability-check)
2 |
3 | [](CONTRIBUTING.md)  [](https://packagist.org/packages/umutphp/wp-vulnerability-check) [](https://packagist.org/packages/umutphp/wp-vulnerability-check) [](https://packagist.org/packages/umutphp/wp-vulnerability-check) [](https://www.codetriage.com/umutphp/wp-vulnerability-check)
4 |
5 | WordPress Vulnerability Check (wp-vulnerability-check) is a console application to check the WPScan Vulnerability Database via API to identify the security issues of WordPress plugins installed.
6 |
7 | If you're using WordPress as part of your application and thrid-party WordPress plugins to implement your bussiness logic, you can run wp-vulnerability-check on a CI pipeline to check the vulnerabilities. You should get a token from [wpscan.com](https://wpscan.com/) in order to have access to the API.
8 |
9 | 
10 |
11 | ---
12 |
13 |
14 |
15 |
16 | **Table Of Contents**
17 |
18 | - [How To Use](#how-to-use)
19 | - [Requirements](#requirements)
20 | - [Installation](#installation)
21 | - [CLI Options](#cli-options)
22 | - [Issues](#issues)
23 | - [Contributing](#contributing)
24 |
25 |
26 |
27 | ---
28 |
29 | ## How To Use
30 |
31 | ### Requirements
32 |
33 | wp-vulnerability-check requires PHP version 5.6.0 or greater.
34 |
35 | ### Installation
36 |
37 | It can be installed as a stand-alone tool or used as a test step on your CI pipeline.
38 |
39 | ```bash
40 | composer require umutphp/wp-vulnerability-check
41 |
42 | ```
43 |
44 | ### CLI Options
45 |
46 | After succesfull installation, you can display the options as follows;
47 |
48 | ```bash
49 | ./wp-vulnerability-check --help
50 | ---------------------------
51 | WP Vulnerability Check version 0.2.2
52 | ---------------------------
53 | Usage: wp-vulnerability-check [options]
54 | Options:
55 | --config Full path for the YAML config file. A sample config
56 | file is .wvc.yml.sample in root folder. CLI arguments
57 | override the values in config file.
58 | --path Full path of your WordPress installation.
59 | --plugins-path Relative path of the plugin folder. It is optional.
60 | Please specify if you don't use default plugin folder.
61 | --mu-plugins-path Relative path of the mu plugin folder. It is optional.
62 | Please specify if you don't use default mu plugin folder.
63 | --themes-path Relative path of the theme folder. It is optional.
64 | Please specify if you don't use default theme folder.
65 | --token Token got from wpscan.com
66 | --exclude Exclude the plugins given in comma separated format.
67 | --output The format of output. Valid values JSON, READABLE, HTML,
68 | NO (Default).
69 | --no-colors Disable the console colors. It is enabled by default.
70 | --version Show version.
71 | --help Print this help.
72 |
73 | ```
74 |
75 | A sample excution,
76 |
77 | ```bash
78 | $ ./wp-vulnerability-check --path /path/to/plugins/ --token token --output readable
79 |
80 | Checking WordPress version ...
81 |
82 | .
83 | ------------------------------------------------------------
84 | Vulnerability Details
85 |
86 |
87 | Checking plugins...
88 |
89 | .......
90 |
91 | Checked 7 plugins in 2 second, no vulnerability found.
92 |
93 | The plugins which are not in WPScan Vulnerability Database; akismet, custom-css-js, hello, multisite-clone-duplicator, wp-migrate-db, base, mu-autoloader.
94 | PS: You can exclude your custom plugins with --exclude parameter.
95 |
96 | Checking theme...
97 |
98 | .
99 |
100 | Checked 1 theme in 0.2 second, no vulnerability found.
101 |
102 | The theme which is not in WPScan Vulnerability Database; simple-days.
103 | PS: You can exclude your custom themes with --exclude parameter.
104 | ```
105 |
106 | ## Issues
107 |
108 | Bug reports and feature requests can be submitted on the [Github Issue Tracker](https://github.com/umutphp/wp-vulnerability-check/issues).
109 |
110 | ## Contributing
111 |
112 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
113 |
114 | ## Code Of Conduct
115 |
116 | See [CODE_OF_CONDUCT](CODE_OF_CONDUCT) for more information.
117 |
--------------------------------------------------------------------------------
/src/Settings.php:
--------------------------------------------------------------------------------
1 | config = trim($arguments->getNext());
76 | break;
77 | default:
78 | break;
79 | }
80 | }
81 |
82 | if ($setting->config !== null) {
83 | if (!file_exists($setting->config)) {
84 | throw new \Exception("The config file (" . $setting->config . ") given does not exists.");
85 | }
86 |
87 | try {
88 | $values = Yaml::parseFile($setting->config);
89 | foreach ($values as $key => $value) {
90 | if(property_exists($setting, $key)){
91 | $setting->{$key} = $value;
92 | }
93 | }
94 | } catch (ParseException $exception) {
95 | throw new \Exception('Unable to parse the YAML string: %s', $exception->getMessage());
96 | }
97 | }
98 |
99 | foreach ($arguments as $argument) {
100 | if ($argument[0] !== '-') {
101 | throw new \Exception("Invalid argument $argument");
102 | } else {
103 | switch ($argument) {
104 | case '--path':
105 | $setting->path = trim($arguments->getNext());
106 | break;
107 | case '--plugins-path':
108 | $setting->plugins = trim($arguments->getNext());
109 | break;
110 | case '--mu-plugins-path':
111 | $setting->MUPlugins = trim($arguments->getNext());
112 | break;
113 | case '--themes-path':
114 | $setting->themes = trim($arguments->getNext());
115 | break;
116 | case '--token':
117 | $setting->token = trim($arguments->getNext());
118 | break;
119 | case '--exclude':
120 | $setting->excluded = explode(',', $arguments->getNext());
121 | break;
122 | case '--output':
123 | $setting->output = trim($arguments->getNext());
124 | break;
125 | case '--no-colors':
126 | $setting->colors = false;
127 | break;
128 | case '--config':
129 | $arguments->getNext();
130 | break;
131 | default:
132 | throw new \Exception("Invalid argument $argument");
133 | }
134 | }
135 | }
136 |
137 | $setting = self::setDefaults($setting);
138 |
139 | return $setting;
140 | }
141 |
142 | /**
143 | * @param Settings $setting
144 | * @return Settings $setting
145 | */
146 | public static function setDefaults(Settings $setting)
147 | {
148 | $setting->plugins = $setting->path . DIRECTORY_SEPARATOR . $setting->plugins;
149 | $setting->MUPlugins = $setting->path . DIRECTORY_SEPARATOR . $setting->MUPlugins;
150 | $setting->themes = $setting->path . DIRECTORY_SEPARATOR . $setting->themes;
151 |
152 | return $setting;
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/Manager.php:
--------------------------------------------------------------------------------
1 | settings = $settings;
20 | }
21 | }
22 |
23 | /**
24 | * Main check function
25 | *
26 | * @param Settings|null $settings
27 | * @param Output|null $output
28 | * @return bool
29 | * @throws Exception\NotExistsPath
30 | */
31 | public function check(Settings $settings = null, Output $output = null)
32 | {
33 | if ($settings instanceof Settings) {
34 | $this->settings = $settings;
35 | }
36 |
37 | if ($output === null) {
38 | $writer = null;
39 |
40 | if ($this->settings->output === 'HTML') {
41 | $writer = new Writer\HTML();
42 | }
43 |
44 | if ($this->settings->colors === true) {
45 | $output = new OutputColored($writer);
46 | } else {
47 | $output = new Output($writer);
48 | }
49 | }
50 |
51 | $returnW = $this->checkWordPress($this->settings, $output);
52 | $returnP = $this->checkPlugins($this->settings, $output);
53 | $returnT = $this->checkThemes($this->settings, $output);
54 |
55 | if (!empty($this->lastApiError)) {
56 | $output->writeNewLine();
57 | $output->writeLine('ERROR: ' . $this->lastApiError);
58 | }
59 |
60 | return $returnW && $returnP && $returnT && empty($this->lastApiError);
61 | }
62 |
63 | /**
64 | * Check if the directory exists
65 | *
66 | * @param string $directory
67 | *
68 | * @throws Exception\NotExistsPath
69 | */
70 | private function checkDirectory($directory, Output $output = null)
71 | {
72 | if (!is_dir($directory)) {
73 | if ($output != null) {
74 | $output->writeNewLine();
75 | $output->writeLine("Invalid folder ($directory) is given.");
76 | $output->writeNewLine();
77 | }
78 |
79 | throw new \Exception("Invalid folder ($directory) is given.");
80 | }
81 | }
82 |
83 | /**
84 | * Private WordPress installation check function
85 | *
86 | * @param Settings $settings
87 | * @param Output $output
88 | * @return bool
89 | * @throws Exception\NotExistsPath
90 | */
91 | private function checkWordPress(Settings $settings, Output $output)
92 | {
93 | $this->checkDirectory($this->settings->path, $output);
94 |
95 | $version_file = realpath($this->settings->path) . DIRECTORY_SEPARATOR . "wp-includes" . DIRECTORY_SEPARATOR . "version.php";
96 |
97 | if (!file_exists($version_file)) {
98 | $output->writeNewLine();
99 | $output->writeLine("Invalid installation folder ($version_file) is given.");
100 | $output->writeNewLine();
101 | throw new \Exception("Invalid installation folder ($version_file) is given.");
102 | }
103 |
104 | $wp_version = $this->getWpVersion($version_file);
105 |
106 | if (false === $wp_version) {
107 | $output->writeNewLine();
108 | $output->writeLine("No valid WordPress version is found.");
109 | $output->writeNewLine();
110 | throw new \Exception("No valid WordPress version is found.");
111 | }
112 |
113 | $output->writeLine(
114 | "Checking WordPress version ..."
115 | );
116 | $output->writeNewLine();
117 |
118 | $failed = false;
119 |
120 | try {
121 | $vulResult = $this->checkWordPressVersion($wp_version, $settings);
122 |
123 | // Get Vulnerabilities and check the WordPress with version
124 | if (count($vulResult[$wp_version]['vulnerabilities'])) {
125 | $failed = true;
126 |
127 | $output->error(); // For vulnerability
128 | } else {
129 | $output->ok(); // For success
130 | }
131 | } catch (\Exception $e) {
132 | $code = $e->getCode();
133 | if ($code == 404) {
134 | // No vulnerability in database
135 | $output->ok();
136 | } else {
137 | $output->fail();
138 | $this->handleApiError($code);
139 | }
140 | }
141 |
142 | if ($failed) {
143 | if ($this->settings->output != 'NO') {
144 | $this->output($vulResult, $output);
145 | }
146 | }
147 |
148 | return !$failed;
149 | }
150 |
151 | /**
152 | * Private theme check function
153 | *
154 | * @param Settings $settings
155 | * @param Output $output
156 | * @return bool
157 | * @throws Exception\NotExistsPath
158 | */
159 | private function checkThemes(Settings $settings, Output $output)
160 | {
161 | $this->checkDirectory($this->settings->themes, $output);
162 |
163 | $files = scandir($this->settings->themes);
164 | $themes = array();
165 |
166 | foreach ($files as $key => $value) {
167 | if (!$this->isComponent($value)) {
168 | continue;
169 | }
170 |
171 | if (is_dir($this->settings->themes . DIRECTORY_SEPARATOR . $value)) {
172 | if (file_exists($this->settings->themes . DIRECTORY_SEPARATOR . $value . DIRECTORY_SEPARATOR . "style.css")) {
173 | $themes[$value] = $value . DIRECTORY_SEPARATOR . "style.css";
174 | }
175 | }
176 | }
177 |
178 | /** @var Result[] $results */
179 | $results = array();
180 | $startTime = microtime(true);
181 | $checkedThemes = 0;
182 | $vulnerableThemes = 0;
183 | $totalThemeCount = count($themes);
184 | $missingThemes = array();
185 |
186 | $output->setTotalPluginCount($totalThemeCount);
187 | $output->writeNewLine();
188 | $output->writeLine(
189 | "Checking " . ($totalThemeCount === 1 ? 'theme' : 'themes') . "..."
190 | );
191 | $output->writeNewLine();
192 | foreach ($themes as $theme => $file) {
193 | try {
194 | $checkedThemes++;
195 | $vulResult = $this->checkTheme($theme, $file, $this->settings);
196 |
197 | // Get Vulnerabilities and check the plugin with version
198 | if (count($vulResult[$theme]['vulnerabilities'])) {
199 | $vulnerableThemes++;
200 | $output->error(); // For vulnerability
201 | } else {
202 | $output->ok(); // For success
203 | continue;
204 | }
205 |
206 | $results = array_merge($results, $vulResult);
207 | } catch (\Exception $e) {
208 | $code = $e->getCode();
209 | if ($code == 404) {
210 | // No vulnerability in database or custom theme
211 | $output->ok();
212 | $missingThemes[$theme] = array("message" => $e->getMessage());
213 | } else {
214 | $output->fail();
215 | $this->handleApiError($code);
216 | }
217 | }
218 | }
219 |
220 | $runTime = round(microtime(true) - $startTime, 1);
221 |
222 | $output->writeNewLine(2);
223 |
224 | $message = "Checked $checkedThemes " . ($checkedThemes === 1 ? 'theme' : 'themes') . " in $runTime second, ";
225 | if ($vulnerableThemes === 0) {
226 | $message .= "no vulnerability found.";
227 | } else {
228 | $message .= "found some vulnerability(s) in $vulnerableThemes ";
229 | $message .= ($vulnerableThemes === 1 ? 'theme.' : 'themes.');
230 | }
231 |
232 | $output->writeLine($message);
233 |
234 | if (!empty($missingThemes)) {
235 | $message = "The " . (count($missingThemes) === 1 ? 'theme which is not ' : 'themes which are not ');
236 | $message .= "in WPScan Vulnerability Database; " . implode(", ", array_keys($missingThemes)). ".";
237 | $output->writeNewLine();
238 | $output->writeLine($message);
239 | $output->writeLine("PS: You can exclude your custom themes with --exclude parameter.");
240 | }
241 |
242 | if (!empty($results)) {
243 | if ($this->settings->output != 'NO') {
244 | $this->output($results, $output);
245 | }
246 |
247 | return false;
248 | }
249 |
250 | return true;
251 | }
252 |
253 | /**
254 | * Private plugin check function
255 | *
256 | * @param Settings $settings
257 | * @param Output $output
258 | * @return bool
259 | * @throws Exception\NotExistsPath
260 | */
261 | private function checkPlugins(Settings $settings, Output $output)
262 | {
263 | $this->checkDirectory($this->settings->plugins, $output);
264 |
265 | $plugins = array_merge(
266 | $this->getPlugins($this->settings->plugins),
267 | $this->getMuPlugins($this->settings->MUPlugins)
268 | );
269 |
270 | /** @var Result[] $results */
271 | $results = array();
272 | $startTime = microtime(true);
273 | $checkedPlugins = 0;
274 | $vulnerablePlugins = 0;
275 | $totalPluginCount = count($plugins);
276 | $missingPlugins = array();
277 |
278 | $output->setTotalPluginCount($totalPluginCount);
279 | $output->writeNewLine();
280 | $output->writeLine(
281 | "Checking " . ($totalPluginCount === 1 ? 'plugin' : 'plugins') . "..."
282 | );
283 | $output->writeNewLine();
284 | foreach ($plugins as $plugin => $meta) {
285 | try {
286 | if (in_array($plugin, $this->settings->excluded)) {
287 | continue;
288 | }
289 |
290 | $checkedPlugins++;
291 | $vulResult = $this->checkPlugin($plugin, $meta, $this->settings);
292 |
293 | // Get Vulnerabilities and check the plugin with version
294 | if (count($vulResult[$plugin]['vulnerabilities'])) {
295 | $vulnerablePlugins++;
296 | $output->error(); // For vulnerability
297 | } else {
298 | $output->ok(); // For success
299 | continue;
300 | }
301 |
302 | $results = array_merge($results, $vulResult);
303 | } catch (\Exception $e) {
304 | $code = $e->getCode();
305 | if ($code == 404) {
306 | // No vulnerability in database or custom plugin
307 | $output->ok();
308 | $missingPlugins[$plugin] = array("message" => $e->getMessage());
309 | } else {
310 | $output->fail();
311 | $this->handleApiError($code);
312 | }
313 | }
314 | }
315 |
316 | $runTime = round(microtime(true) - $startTime, 1);
317 |
318 | $output->writeNewLine(2);
319 |
320 | $message = "Checked $checkedPlugins plugins in $runTime second, ";
321 | if ($vulnerablePlugins === 0) {
322 | $message .= "no vulnerability found.";
323 | } else {
324 | $message .= "found some vulnerability(s) in $vulnerablePlugins ";
325 | $message .= ($vulnerablePlugins === 1 ? 'plugin.' : 'plugins.');
326 | }
327 |
328 | $output->writeLine($message);
329 |
330 | if (!empty($missingPlugins)) {
331 | $message = "The " . (count($missingPlugins) === 1 ? 'plugin which is not ' : 'plugins which are not ');
332 | $message .= "in WPScan Vulnerability Database; " . implode(", ", array_keys($missingPlugins)). ".";
333 | $output->writeNewLine();
334 | $output->writeLine($message);
335 | $output->writeLine("PS: You can exclude your custom plugins with --exclude parameter.");
336 | }
337 |
338 | if (!empty($results)) {
339 | if ($this->settings->output != 'NO') {
340 | $this->output($results, $output);
341 | }
342 |
343 | return false;
344 | }
345 |
346 | return true;
347 | }
348 |
349 | /**
350 | * Return true if the filename is most probably a plugin
351 | *
352 | * @param string $plugin File name
353 | * @return boolean
354 | */
355 | public function isComponent($plugin) {
356 | if ($plugin === '.' || $plugin === '..' || $plugin === 'index.php') {
357 | return false;
358 | }
359 |
360 | if (is_array($this->settings->excluded)) {
361 | if (in_array($plugin, $this->settings->excluded)) {
362 | return false;
363 | }
364 | }
365 |
366 | return true;
367 | }
368 |
369 | /**
370 | * Check the vulnerabities array and remove the fixed bugs
371 | *
372 | * @param array $result Result array for the plugin
373 | * @param string $version Version the plugin to be checked
374 | * @return array
375 | */
376 | public function checkVulnerabilityWithVersion($result, $version)
377 | {
378 | foreach ($result['vulnerabilities'] as $key => $value) {
379 | if ($value['fixed_in'] && version_compare($version, $value['fixed_in'], '>=')) {
380 | unset($result['vulnerabilities'][$key]);
381 | }
382 | }
383 |
384 | return $result;
385 | }
386 |
387 | /**
388 | * Check WordPress with version via API
389 | *
390 | * @param string $version
391 | * @param Settings $settings
392 | * @return Result[]
393 | * @throws Exception
394 | */
395 | public function checkWordPressVersion($version, Settings $settings = null)
396 | {
397 | if ($settings instanceof Settings) {
398 | $this->settings = $settings;
399 | }
400 |
401 | $versionSlug = str_replace('.', '', $version);
402 |
403 | $vulnerabilities = $this->get($versionSlug, $this->settings->token, 'wordpresses');
404 |
405 | if (!isset($vulnerabilities[$version])) {
406 | throw new \Exception($vulnerabilities["status"], $vulnerabilities["_code"]);
407 | }
408 |
409 | return $vulnerabilities;
410 | }
411 |
412 | /**
413 | * Check theme with theme name via API
414 | *
415 | * @param string $themeName
416 | * @param string $themeFile
417 | * @param Settings $settings
418 | * @return Result[]
419 | * @throws Exception
420 | */
421 | public function checkTheme($themeName, $themeFile, Settings $settings = null)
422 | {
423 | if ($settings instanceof Settings) {
424 | $this->settings = $settings;
425 | }
426 |
427 | $meta = $this->getThemeMetaData(
428 | $this->settings->themes . DIRECTORY_SEPARATOR . $themeFile
429 | );
430 |
431 | $vulnerabilities = $this->get($themeName, $this->settings->token, 'themes');
432 |
433 | if (!isset($vulnerabilities[$themeName])) {
434 | throw new \Exception($vulnerabilities["status"], $vulnerabilities["_code"]);
435 | }
436 |
437 | $vulnerabilities[$themeName] = $this->checkVulnerabilityWithVersion($vulnerabilities[$themeName], $meta['Version']);
438 |
439 | return $vulnerabilities;
440 | }
441 |
442 | /**
443 | * Check plugin with plugin name via API
444 | *
445 | * @param string $pluginName
446 | * @param array $meta
447 | * @param Settings $settings
448 | * @return Result[]
449 | * @throws Exception
450 | */
451 | public function checkPlugin($pluginName, $meta, Settings $settings = null)
452 | {
453 | if ($settings instanceof Settings) {
454 | $this->settings = $settings;
455 | }
456 |
457 | $vulnerabilities = $this->get($pluginName, $this->settings->token);
458 |
459 | if (!isset($vulnerabilities[$pluginName])) {
460 | throw new \Exception($vulnerabilities["status"], $vulnerabilities["_code"]);
461 | }
462 |
463 | $vulnerabilities[$pluginName] = $this->checkVulnerabilityWithVersion($vulnerabilities[$pluginName], $meta['Version']);
464 |
465 | return $vulnerabilities;
466 | }
467 |
468 | /**
469 | * Get theme meta data from the header comment block
470 | *
471 | * @param string $file File path of the plugin file
472 | * @return array The array of the meta values
473 | */
474 | private function getThemeMetaData($file)
475 | {
476 | $all_headers = array(
477 | 'Name' => 'Theme Name',
478 | 'ThemeURI' => 'Theme URI',
479 | 'Version' => 'Version',
480 | 'Description' => 'Description',
481 | 'Author' => 'Author',
482 | 'AuthorURI' => 'Author URI',
483 | 'TextDomain' => 'Text Domain',
484 | 'DomainPath' => 'Domain Path',
485 | 'Network' => 'Network',
486 | // Site Wide Only is deprecated in favor of Network.
487 | '_sitewide' => 'Site Wide Only'
488 | );
489 |
490 | return $this->getFileMetaData($all_headers, $file);
491 | }
492 |
493 | /**
494 | * Get plugin meta data from the header comment block
495 | *
496 | * @param string $file File path of the plugin file
497 | * @return array The array of the meta values
498 | */
499 | private function getPluginMetaData($file)
500 | {
501 | $all_headers = array(
502 | 'Name' => 'Plugin Name',
503 | 'PluginURI' => 'Plugin URI',
504 | 'Version' => 'Version',
505 | 'Description' => 'Description',
506 | 'Author' => 'Author',
507 | 'AuthorURI' => 'Author URI',
508 | 'TextDomain' => 'Text Domain',
509 | 'DomainPath' => 'Domain Path',
510 | 'Network' => 'Network',
511 | // Site Wide Only is deprecated in favor of Network.
512 | '_sitewide' => 'Site Wide Only'
513 | );
514 |
515 | return $this->getFileMetaData($all_headers, $file);
516 | }
517 |
518 | /**
519 | * Get file meta data from the header comment block
520 | *
521 | * @param string $file File path of the plugin file
522 | * @return array The array of the meta values
523 | */
524 | private function getFileMetaData($headers, $file)
525 | {
526 | // We don't need to write to the file, so just open for reading.
527 | $fp = fopen($file, 'r');
528 |
529 | // Pull only the first 8kiB of the file in.
530 | $file_data = fread($fp, 8192);
531 |
532 | // PHP will close file handle, but we are good citizens.
533 | fclose($fp);
534 |
535 | // Make sure we catch CR-only line endings.
536 | $file_data = str_replace("\r", "\n", $file_data);
537 |
538 | foreach ($headers as $field => $regex) {
539 | if (preg_match( '/^[ \t\/*#@]*' . preg_quote($regex, '/') . ':(.*)$/mi', $file_data, $match) && $match[1])
540 | $headers[$field] = trim(preg_replace("/\s*(?:\*\/|\?>).*/", '', $match[1]));
541 | else
542 | $headers[$field] = '';
543 | }
544 |
545 | return $headers;
546 | }
547 |
548 | /**
549 | * Private function to get the result from the API
550 | *
551 | * @param string $name
552 | * @param string $APIToken
553 | * @param string $type
554 | * @return array Array decoded from JSON string
555 | */
556 | private function get($name, $APIToken, $type = "plugins") {
557 | $ch = curl_init();
558 | $url = self::WPSCAN_API_URL . $type . '/' . $name;
559 |
560 | curl_setopt($ch, CURLOPT_URL, $url);
561 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
562 |
563 | $headers = array(
564 | 'Authorization: Token token=' . $APIToken
565 | );
566 |
567 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
568 |
569 | $response = curl_exec($ch);
570 | if ($response === false) {
571 | $errorCode = curl_errno($ch);
572 | curl_close($ch);
573 |
574 | return ['_code' => $errorCode, 'status' => 'cURL error'];
575 | }
576 |
577 | $data = json_decode($response, true);
578 | $data['_code'] = curl_getinfo($ch, CURLINFO_HTTP_CODE);
579 |
580 | curl_close($ch);
581 |
582 | return $data;
583 | }
584 |
585 | /**
586 | * Print result list
587 | *
588 | * @param array $results
589 | * @param Output $output
590 | * @return void
591 | */
592 | private function output($results, $output) {
593 | if ($this->settings->output === 'READABLE') {
594 | $output->writeNewLine();
595 | $output->writeLine(str_repeat('-', 60));
596 | $output->writeLine("Vulnerability Details");
597 | $output->writeNewLine();
598 | }
599 |
600 | foreach ($results as $plugin => $vulnerabilities) {
601 | if (!is_array($vulnerabilities)) {
602 | continue;
603 | }
604 | $output->writeResult($plugin, $vulnerabilities['vulnerabilities']);
605 | }
606 | }
607 |
608 | /**
609 | * Check the plugins directory and retrieve all plugin files with plugin data.
610 | *
611 | * WordPress only supports plugin files in the base plugins directory
612 | * (wp-content/plugins) and in one directory above the plugins directory
613 | * (wp-content/plugins/my-plugin). The file it looks for has the plugin data
614 | * and must be found in those two locations.
615 | *
616 | * @param string $pluginRoot Absolute path to plugins folder.
617 | * @return array[] Array of arrays of plugin data, keyed by plugin file name. See `get_plugin_data()`.
618 | */
619 | private function getPlugins($pluginRoot)
620 | {
621 | $wpPlugins = array();
622 |
623 | // Files in wp-content/plugins directory.
624 | $pluginsDir = @opendir($pluginRoot);
625 | $pluginFiles = array();
626 |
627 | if ($pluginsDir) {
628 | while (($file = readdir($pluginsDir)) !== false) {
629 | if ('.' === substr($file, 0, 1)) {
630 | continue;
631 | }
632 |
633 | if (is_dir($pluginRoot . '/' . $file)) {
634 | $pluginsSubdir = @opendir($pluginRoot . '/' . $file);
635 |
636 | if ($pluginsSubdir) {
637 | while (($subfile = readdir($pluginsSubdir)) !== false) {
638 | if ('.' === substr($subfile, 0, 1)) {
639 | continue;
640 | }
641 |
642 | if ('.php' === substr($subfile, -4)) {
643 | $pluginFiles[] = "$file/$subfile";
644 | }
645 | }
646 |
647 | closedir($pluginsSubdir);
648 | }
649 | } else {
650 | if ('.php' === substr($file, -4)) {
651 | $pluginFiles[] = $file;
652 | }
653 | }
654 | }
655 |
656 | closedir($pluginsDir);
657 | }
658 |
659 | if (empty($pluginFiles)) {
660 | return $wpPlugins;
661 | }
662 |
663 | foreach ($pluginFiles as $pluginFile) {
664 | if (!is_readable("$pluginRoot/$pluginFile")) {
665 | continue;
666 | }
667 |
668 | $pluginData = $this->getPluginMetaData("$pluginRoot/$pluginFile");
669 |
670 | if (empty($pluginData['Name'])) {
671 | continue;
672 | }
673 |
674 | $wpPlugins[$this->pluginBasename($pluginFile)] = $pluginData;
675 | }
676 |
677 | return $wpPlugins;
678 | }
679 |
680 | /**
681 | * Check the mu-plugins directory and retrieve all mu-plugin files with any plugin data.
682 | *
683 | * WordPress only includes mu-plugin files in the base mu-plugins directory (wp-content/mu-plugins).
684 | *
685 | * @param string $muPluginRoot Absolute path to mu-plugins folder.
686 | * @return array[] Array of arrays of mu-plugin data, keyed by plugin file name.
687 | */
688 | private function getMuPlugins($muPluginRoot)
689 | {
690 | $wpPlugins = array();
691 | $pluginFiles = array();
692 |
693 | if (!is_dir($muPluginRoot)) {
694 | return $wpPlugins;
695 | }
696 |
697 | // Files in wp-content/mu-plugins directory.
698 | $pluginsDir = @opendir($muPluginRoot);
699 | if ($pluginsDir) {
700 | while (($file = readdir($pluginsDir)) !== false) {
701 | if ('.php' === substr($file, -4)) {
702 | $pluginFiles[] = $file;
703 | }
704 | }
705 | } else {
706 | return $wpPlugins;
707 | }
708 |
709 | closedir($pluginsDir);
710 |
711 | if (empty($pluginFiles)) {
712 | return $wpPlugins;
713 | }
714 |
715 | foreach ($pluginFiles as $pluginFile) {
716 | if (!is_readable($muPluginRoot . "/$pluginFile")) {
717 | continue;
718 | }
719 |
720 | $pluginData = $this->getPluginMetaData($muPluginRoot . "/$pluginFile");
721 |
722 | if (empty($pluginData['Name'])) {
723 | $pluginData['Name'] = $pluginFile;
724 | }
725 |
726 | $wpPlugins[$this->pluginBasename($pluginFile)] = $pluginData;
727 | }
728 |
729 | if (isset($wpPlugins['index']) && filesize($muPluginRoot . '/index.php') <= 30) {
730 | // Silence is golden.
731 | unset($wpPlugins['index']);
732 | }
733 |
734 | return $wpPlugins;
735 | }
736 |
737 | /**
738 | * Gets the basename of a plugin.
739 | *
740 | * This method extracts the name of a plugin from its filename.
741 | *
742 | * @param string $pluginFile The filename of plugin.
743 | * @return string The name of a plugin.
744 | */
745 | private function pluginBasename($pluginFile)
746 | {
747 | if (false === strpos($pluginFile, '/')) {
748 | return basename($pluginFile, '.php');
749 | } else {
750 | return dirname($pluginFile);
751 | }
752 | }
753 |
754 | private function handleApiError($code)
755 | {
756 | switch ($code) {
757 | case 401:
758 | $this->lastApiError = 'API Token expired.';
759 | break;
760 | case 403:
761 | $this->lastApiError = 'Invalid API Token.';
762 | break;
763 | case 404:
764 | // No the plugin/theme found, ignored.
765 | break;
766 | case 429:
767 | $this->lastApiError = 'Daily API usage limit hit.';
768 | break;
769 | case 500:
770 | $this->lastApiError = 'WPScan API error. Status: 500.';
771 | break;
772 | case 502:
773 | $this->lastApiError = 'WPScan API error. Status: 502.';
774 | break;
775 | case ($code < 100):
776 | $this->lastApiError = 'Unable to connect to the WPScan API. cURL error: ' . $code . '.';
777 | break;
778 | default:
779 | $this->lastApiError = 'Unknown response from the WPScan API. Status: ' . $code . '.';
780 | break;
781 | }
782 | }
783 |
784 | /**
785 | * Get WordPress version without evaluating version.php file which can be unsafe.
786 | *
787 | * @param string $file path to WordPress wp-includes/version.php file
788 | * @return false|string
789 | */
790 | private function getWpVersion($file)
791 | {
792 | $contents = file_get_contents($file);
793 |
794 | if (preg_match('/\$wp_version\s+=\s+\'(.+?)\';/mi', $contents, $match)) {
795 | return $match[1];
796 | }
797 |
798 | return false;
799 | }
800 | }
801 |
--------------------------------------------------------------------------------