├── _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 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CONTRIBUTING.md) ![WOSPM Checker](https://github.com/umutphp/wp-vulnerability-check/workflows/WOSPM%20Checker/badge.svg) [![Latest Stable Version](https://poser.pugx.org/umutphp/wp-vulnerability-check/v/stable)](https://packagist.org/packages/umutphp/wp-vulnerability-check) [![Total Downloads](https://poser.pugx.org/umutphp/wp-vulnerability-check/downloads)](https://packagist.org/packages/umutphp/wp-vulnerability-check) [![composer.lock](https://poser.pugx.org/umutphp/wp-vulnerability-check/composerlock)](https://packagist.org/packages/umutphp/wp-vulnerability-check) [![Open Source Helpers](https://www.codetriage.com/umutphp/wp-vulnerability-check/badges/users.svg)](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 | ![WordPress Vulnerability Check](./assets/wvc_banner.png "WordPress Vulnerability Check") 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 | --------------------------------------------------------------------------------