diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index c1d4036..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Web server integration tests - -on: - pull_request: - branches: - - master - -jobs: - container-job: - runs-on: ubuntu-latest - steps: - - name: Setup ddev - uses: jonaseberle/github-action-setup-ddev@v1 - run: ddev --help -# services: -# mysql: -# image: mysql:5.7 -# env: -# MYSQL_DATABASE: testdb -# MYSQL_USER: testdb -# MYSQL_PASSWORD: mysqlpass -# MYSQL_ROOT_PASSWORD: mysqlpass -# ports: -# - 3306:3306 -# options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 \ No newline at end of file diff --git a/README.md b/README.md index f4463dd..74a0160 100644 --- a/README.md +++ b/README.md @@ -4,95 +4,148 @@ Manages common security aspects of WordPress. Supports nginx and Apache. ## Basic Usage This package implements the following commands: -**wp secure block_access_to_htaccess** +### Deploy All Security rules -Blocks access to `.htaccess` and `nginx.conf` files. +Deploys all above-mentioned rules at once. -``` -wp secure block_access_to_htaccess [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] +```bash +wp secure all ``` -**wp secure block_access_to_sensitive_directories** +### Remove All Security Rules -Blocks direct access to sensitive directories - `.git`, `svn`, `cache` and `vendors` +Removes all security rules. +```bash +wp secure flush ``` -wp secure block_access_to_sensitive_directories [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] + +### Block the access to sensitive files and directories +```bash +wp secure block-access ``` -**wp secure block_access_to_sensitive_files** +By default, this command blocks the direct access to sensitive files and directories: +`readme.txt`, `readme.html`, `xmlrpc.php`, `wp-config.php`, `wp-admin/install.php`, `wp-admin/upgrade.php`, `.git`, `svn`, `cache` and `vendors` -Blocks direct access to sensitive files - `readme.txt`, `readme.html`, `wp-config.php`, `wp-admin/install.php` and `wp-admin/upgrade.php` +Possible options are: +- sensitive-files +- sensitive-directories +- xmlrpc +- htaccess +- custom +- all (does all the above) -``` -wp secure block_access_to_sensitive_files [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] +Examples: + +```bash +wp secure block-access sensitive-files +wp secure block-access sensitive-directories +wp secure block-access xmlrpc +wp secure block-access htaccess +wp secure block-access all ``` -**wp secure block_access_to_xmlrpc** +However, you can also block custom files and/or folders of your choice. To do that you should use `custom` argument +and pass one of two additional options `--files` and/or `--directories`. -Blocks direct access XML-RPC +If you want to block custom files, make sure that you pass only file names, not a full file paths. -``` -wp secure block_access_to_xmlrpc [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] -``` +Examples: -### wp secure block_author_scanning +````bash +wp secure block-access custom --files=dump.sql,phpinfo.php,adminer.php +wp secure block-access custom --directories=wp-content/mu-plugins +```` -Blocks author scanning. Author scanning is a common technique of brute force attacks on WordPress. It is used to crack passwords for the known usernames and to gather additional information about the WordPress itself. +### Block Author Scanning -``` -wp secure block_author_scanning [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] +```bash +wp secure block-author-scanning ``` -### wp secure block_php_execution_in_plugins +Blocks author scanning. Author scanning is a common technique of brute force attacks on WordPress. It is used to crack passwords for the known usernames and to gather additional information about the WordPress itself. -Blocks direct access and execution of PHP files in `wp-content/plugins` directory. +Examples: +```bash +wp secure block-author-scanning ``` -wp secure block_php_execution_in_plugins [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] + +### Block Direct Access and Execution in certain directories + +```bash +wp secure block-php-execution ``` -### wp secure block_php_execution_in_uploads +Blocks direct access and execution of PHP files in `wp-content/plugins`, `wp-content/uploads`, `wp-content/themes` and `wp-includes` directories. + +You need to specify where you want to prevent direct access to PHP files. Possible options are: +- all +- plugins +- uploads +- themes +- wp-includes -Blocks direct access and execution of PHP files in `wp-content/uploads` directory. +Examples: +```bash +wp secure block-php-execution all +wp secure block-php-execution plugins +wp secure block-php-execution uploads +wp secure block-php-execution themes +wp secure block-php-execution wp-includes ``` -wp secure block_php_execution_in_uploads [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] + +### Disable Directory Browsing +```bash +wp secure disable-directory-browsing ``` -### wp secure block_php_execution_in_themes +Disables directory browsing. -Blocks direct access and execution of PHP files in `wp-content/themes` directory. +By default, when your web server does not find an index file (i.e. a file like index.php or index.html), it +automatically displays an index page showing the contents of the directory. +This could make your site vulnerable to hack attacks by revealing important information needed to exploit a vulnerability in a WordPress plugin, theme, or your server in general. +Examples: + +```bash +wp secure disable-directory-browsing ``` -wp secure block_php_execution_in_themes [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] -``` -### wp secure block_php_execution_in_wp_includes -Blocks direct access and execution of PHP files in include directories - `wp-admin/includes`, `wp-includes/*.php`, `wp-includes/js/tinymce/langs/*.php`, `wp-includes/theme-compat` +### Disable WordPress File Editor + +Disables the WordPress file editor. It could be used to edit arbitrary files using the web interface. +This makes it easier for attackers to change files on the server using a web browser. +```bash +wp secure disable-file-editor ``` -wp secure block_php_execution_in_wp_includes [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] + +### Fix file and directory permissions + +```bash +wp secure fix-permissions ``` -### wp secure disable_directory_browsing +Use this command to verify that the permissions of all files and directories are set according the WordPress recommendations. +This command will set **0666** to all files and **0755** to all folders inside WordPress installation. -Disables directory browsing. +**IMPORTANT: Don't use this command if you don't know what you are doing here!** -By default when your web server does not find an index file (i.e. a file like index.php or index.html), it -automatically displays an index page showing the contents of the directory. -This could make your site vulnerable to hack attacks by revealing important information needed to exploit a vulnerability in a WordPress plugin, theme, or your server in general. +### Check the integrity of WordPress files -``` -wp secure disable_directory_browsing [--remove] [--file-path=/alternative/path] [--output] [--server=apache|nginx] -``` +Downloads MD5 checksums for the current version from WordPress.org, and compares those checksums against the currently +installed files. -### wp secure flush +It also returns a list of files that shouldn't be part of default WordPress installation, which can be very useful when you are +looking for a possible injected files. -Removes all security rules. +Examples: -``` -wp secure flush +```bash +wp secure integrity-scan ``` ## Global options @@ -100,24 +153,23 @@ wp secure flush ### Remove single security rule Using `--remove` with any rule command, you can remove it from configuration. -``` -wp secure block_php_execution_in_wp_includes --remove +```bash +wp secure block-access xmlrpc --remove ``` ### Get the output instead of writing in configuration files Using `--output` option with any rule command, you can see actual rule code which you can inspect or manually copy to any file of your choice. -``` -wp secure block_php_execution_in_wp_includes --output -wp secure block_php_execution_in_wp_includes --output --server=nginx +```bash +wp secure block-access htaccess --output +wp secure block-access htaccess --output --server=nginx ``` ### Specify server type By default, all rules are generated for Apache or LiteSpeed web servers that utilize `.htaccess` file. However, you can use `--server` to specify nginx if you want. -``` -wp secure block_php_execution_in_wp_includes --server=nginx -wp secure block_php_execution_in_wp_includes --server=--file-path=/home/user/mysite.com/nginx.conf +```bash +wp secure block-access htaccess --server=nginx ``` ### Specify custom file path @@ -125,11 +177,11 @@ By default, all commands assume that rules should be written in the root of Word However, you can specify a custom file path that is going to be used for storing security rules. ``` -wp secure block_php_execution_in_plugins --file-path=/home/user/mysite.com/.htaccess +wp secure block-access htaccess --file-path=/home/user/mysite.com/.htaccess ``` ## Important Note for nginx users -nginx rules are stored in the `nginx.conf` file. However, for rules to actually work, you need to manually include this file in your vhost configuration and then restart nginx server: +The nginx rules are stored in the `nginx.conf` file. However, for rules to actually work, you need to manually include this file in your vhost configuration and then restart nginx server: ``` systemctl restart nginx ``` @@ -159,5 +211,4 @@ These paths and URLs are going to be used during tests, so make sure that they a ## Contributing We appreciate you taking the initiative to contribute to this project. -Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. - +Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. \ No newline at end of file diff --git a/src/FileManager.php b/src/FileManager.php index 14df81c..4767224 100644 --- a/src/FileManager.php +++ b/src/FileManager.php @@ -82,7 +82,7 @@ private function setFileContent() : array { } if(!$this->isReadable()) { - throw new (FileIsNotReadable::class); + throw new FileIsNotReadable(); } return $this->read(); @@ -156,7 +156,7 @@ private function fileExist() : bool { * * @return array|string */ - private static function removeZeroSpace($content): array|string + private static function removeZeroSpace($content) { if(is_array($content)) { return array_map([static::class, 'removeZeroSpace'], $content); @@ -203,9 +203,9 @@ public function extractRuleBlock(string $marker) : array { * * @param string $needle * - * @return int|bool + * @return bool|int|string */ - private function findInFile(string $needle) : int|bool { + private function findInFile(string $needle) { return array_search($needle, $this->file); } @@ -214,7 +214,7 @@ private function findInFile(string $needle) : int|bool { * * @return array|bool */ - public function extractSecureBlock(): bool|array { + public function extractSecureBlock() { $start = $this->findInFile(self::MARKER_GLOBAL_START . self::SPACE_DELIMITER . self::MARKER_WP_CLI_SECURE); $end = $this->findInFile(self::MARKER_GLOBAL_END . self::SPACE_DELIMITER . self::MARKER_WP_CLI_SECURE); @@ -330,7 +330,7 @@ private function backup() : bool public function add(array $content, string $marker = ''): bool { //If the rule block already exist, there is no reason to add it again if($this->hasRuleBlock($marker)) { - throw new(RuleAlreadyExist::class); + throw new RuleAlreadyExist(); } //Check if file exist? @@ -339,7 +339,7 @@ public function add(array $content, string $marker = ''): bool { } if(!$this->isWritable()) { - throw new(FileIsNotWritable::class); + throw new FileIsNotWritable(); } //Wrap the rule block with markers @@ -450,4 +450,4 @@ private function flattenArray(array $array, int $depth = 1) : array { return $result; } -} \ No newline at end of file +} diff --git a/src/SecureCommand.php b/src/SecureCommand.php index 729aba1..433e879 100644 --- a/src/SecureCommand.php +++ b/src/SecureCommand.php @@ -3,9 +3,8 @@ namespace WP_CLI_Secure; use WP_CLI; -use WP_CLI\Process; -use WP_CLI\Utils; use WP_CLI_Command; +use WP_CLI_Secure\SubCommands\BlockAccessToCustomSensitiveFiles; use WP_CLI_Secure\SubCommands\BlockAccessToHtaccess; use WP_CLI_Secure\SubCommands\BlockAccessToSensitiveDirectories; use WP_CLI_Secure\SubCommands\BlockAccessToSensitiveFiles; @@ -16,6 +15,7 @@ use WP_CLI_Secure\SubCommands\BlockPhpExecutionInUploads; use WP_CLI_Secure\SubCommands\BlockPhpExecutionInWpIncludes; use WP_CLI_Secure\SubCommands\DisableDirectoryBrowsing; +use WP_CLI_Secure\SubCommands\FixFileAndDirPermissions; use WP_CLI_Secure\SubCommands\Flush; /** @@ -34,7 +34,7 @@ * Success: Directory Browsing is disabled. * * # Remove security rule - * $ wp secure disable-directory-browsing --disable + * $ wp secure disable-directory-browsing --remove * Success: Directory Browsing is enabled. * * # Remove all security rules @@ -77,9 +77,10 @@ class SecureCommand extends WP_CLI_Command { * * ## EXAMPLES * - * $ wp secure disable_directory_browsing + * $ wp secure disable-directory-browsing * Success: Directory Browsing security rule is now active. * + * @subcommand disable-directory-browsing * @when before_wp_load */ public function disable_directory_browsing($args, $assoc_args) : void { @@ -87,77 +88,14 @@ public function disable_directory_browsing($args, $assoc_args) : void { } /** - * Disables execution of PHP files in Themes. + * Disables execution of PHP files in Plugins, Uploads, Themes and wp-includes. * - * PHP files in themes directory shouldn't be directly accessible. This is important in case of malware injection as it prevents attacker from directly - * accessing infected PHP files + * PHP files in certain directories shouldn't be directly accessible. This is important in case of malware injection as it prevents attacker from directly accessing infected PHP files * * ## OPTIONS * - * [--remove] - * : Removes the rule from .htaccess or nginx.conf. - * - * [--output] - * : Use this option to display the actual code that you can manually copy and paste into some other file - * - * [--file-path=] - * : Set a custom path to the file which command should use to write rules into - * - * [--server=] - * : Set a server type. Possible options are "apache" and "nginx". Default is "apache" and all rules are stored in - * .htaccess file - * - * ## EXAMPLES - * - * $ wp secure block_php_execution_in_themes - * Success: Block Execution In Themes rule has been deployed. - * - * @when before_wp_load - */ - public function block_php_execution_in_themes($args, $assoc_args) : void { - (new BlockPhpExecutionInThemes($assoc_args))->output(); - } - - /** - * Disables execution of PHP files in Uploads. - * - * PHP files in `wp-content/uploads` directory shouldn't be directly accessible. This is important in case of malware injection as it prevents attacker from - * directly - * accessing infected PHP files - * - * ## OPTIONS - * - * [--remove] - * : Removes the rule from .htaccess or nginx.conf. - * - * [--output] - * : Use this option to display the actual code that you can manually copy and paste into some other file - * - * [--file-path=] - * : Set a custom path to the file which command should use to write rules into - * - * [--server=] - * : Set a server type. Possible options are "apache" and "nginx". Default is "apache" and all rules are stored in - * .htaccess file - * - * ## EXAMPLES - * - * $ wp secure block_php_execution_in_uploads - * Success: Block Execution In Uploads Directory rule has been deployed. - * - * @when before_wp_load - */ - public function block_php_execution_in_uploads($args, $assoc_args) : void { - (new BlockPhpExecutionInUploads($assoc_args))->output(); - } - - /** - * Disables execution of PHP files in Plugins. - * - * PHP files in `wp-content/plugins` directory shouldn't be directly accessible. This is important in case of malware injection as it prevents attacker - * from directly accessing infected PHP files - * - * ## OPTIONS + * + * : Required. Accepts: plugins, uploads, includes, themes or all. * * [--remove] * : Removes the rule from .htaccess or nginx.conf. @@ -174,54 +112,65 @@ public function block_php_execution_in_uploads($args, $assoc_args) : void { * * ## EXAMPLES * - * $ wp secure block_php_execution_in_plugins + * # Apply the block rules for plugins directory + * $ wp secure block-php plugins * Success: Block Execution In Plugins Directory rule has been deployed. * - * @when before_wp_load - */ - public function block_php_execution_in_plugins($args, $assoc_args) : void { - (new BlockPhpExecutionInPlugins($assoc_args))->output(); - } - - /** - * Disables execution of PHP files in wp-includes directory. - * - * PHP files in `wp-includes` directory shouldn't be directly accessible. This is important in case of malware injection as it prevents attacker - * from directly accessing infected PHP files - * - * ## OPTIONS - * - * [--remove] - * : Removes the rule from .htaccess or nginx.conf. - * - * [--output] - * : Use this option to display the actual code that you can manually copy and paste into some other file - * - * [--file-path=] - * : Set a custom path to the file which command should use to write rules into - * - * [--server=] - * : Set a server type. Possible options are "apache" and "nginx". Default is "apache" and all rules are stored in - * .htaccess file - * - * ## EXAMPLES - * - * $ wp secure block_php_execution_in_wp_includes - * Success: Block Execution In wp-includes Directory rule has been deployed. + * # Apply the block rules for all directories + * $ wp secure block-php all * + * @subcommand block-php-execution * @when before_wp_load */ - public function block_php_execution_in_wp_includes($args, $assoc_args) : void { - (new BlockPhpExecutionInWpIncludes($assoc_args))->output(); + public function block_php($args, $assoc_args) : void { + $blockPart = $args[0]; + + $allowedArguments = [ + 'plugins', 'uploads', 'wp-includes', 'themes', 'all' + ]; + + // Failure first. + if(!in_array($blockPart, $allowedArguments, true)) { + WP_CLI::error( + sprintf('Invalid block part "%s" was provided. Allowed values are "plugins", "uploads", "includes", "themes" or "all"', + $blockPart) + ); + } + + if(in_array($blockPart, ['all', 'plugins'])) { + WP_CLI::debug('Securing the plugins folder.', 'secure'); + (new BlockPhpExecutionInPlugins($assoc_args))->output(); + } + + if(in_array($blockPart, ['all', 'uploads'])) { + WP_CLI::debug('Securing the uploads folder.', 'secure'); + (new BlockPhpExecutionInUploads($assoc_args))->output(); + } + + if(in_array($blockPart, ['all', 'wp-includes'])) { + WP_CLI::debug('Securing the wp-includes folder.', 'secure'); + (new BlockPhpExecutionInWpIncludes($assoc_args))->output(); + } + + if(in_array($blockPart, ['all', 'themes'])) { + WP_CLI::debug('Securing the themes folder.', 'secure'); + (new BlockPhpExecutionInThemes($assoc_args))->output(); + } } /** - * Blocks direct access to sensitive files. + * Blocks direct access to various sensitive files and directories * - * Blocks direct access to readme.html, readme.txt, wp-config.php and wp-admin/install.php files. + * Blocks direct access to sensitive files such as readme.html, readme.txt, wp-config.php and wp-admin/install.php files. + * It also blocks the direct access to a certain number of directories such as .git, svn, cache and vendors. + * + * You can use this command to block access to custom files and folders as well. * * ## OPTIONS * + * + * : This option is required. Accepts one of the following values: sensitive-files, sensitive-directories, htaccess, xmlrpc, custom or all. + * * [--remove] * : Removes the rule from .htaccess or nginx.conf. * @@ -240,19 +189,68 @@ public function block_php_execution_in_wp_includes($args, $assoc_args) : void { * * ## EXAMPLES * - * $ wp secure block_access_to_sensitive_files + * # Secure the sensitive files. + * $ wp secure block-access files + * Success: Block Access to Sensitive Files rule has been deployed. + * + * # Secure all files & directories. + * $ wp secure block-access all + * Success: Block Access to Sensitive Files rule has been deployed. + * + * # Block custom files and directories + * $ wp secure block-access custom --files=dump.sql --directories=some/directory * Success: Block Access to Sensitive Files rule has been deployed. * + * @subcommand block-access * @when before_wp_load */ - public function block_access_to_sensitive_files($args, $assoc_args): void { - (new BlockAccessToSensitiveFiles($assoc_args))->output(); + public function block_access($args, $assoc_args): void { + $blockPart = $args[0]; + + $allowedSubArguments = [ + 'sensitive-files', 'sensitive-directories', 'htaccess', 'xmlrpc', 'all', 'custom' + ]; + + // Failure first. + if(!in_array( $blockPart, $allowedSubArguments, true)) { + WP_CLI::error(sprintf('Invalid block part "%s" was provided. Allowed values are ' . implode(', ', $allowedSubArguments), $blockPart)); + } + + if(in_array($blockPart, ['all', 'sensitive-files'])) { + WP_CLI::debug('Blocking access to the sensitive files.', 'secure'); + (new BlockAccessToSensitiveFiles($assoc_args))->output(); + } + + if(in_array($blockPart, ['all', 'sensitive-directories'])) { + WP_CLI::debug('Blocking access to the directories.', 'secure'); + (new BlockAccessToSensitiveDirectories($assoc_args))->output(); + } + + if(in_array($blockPart, ['all', 'htaccess'])) { + WP_CLI::debug('Blocking access to the htaccess.', 'secure'); + (new BlockAccessToHtaccess($assoc_args))->output(); + } + + if(in_array($blockPart, ['all', 'xmlrpc'])) { + WP_CLI::debug('Blocking access to the xmlrpc.', 'secure'); + (new BlockAccessToXmlRpc($assoc_args))->output(); + } + + //Custom files and directories blocking + if($blockPart === 'custom' && isset($assoc_args['files'])) { + (new BlockAccessToCustomSensitiveFiles($assoc_args))->output(); + } + + if($blockPart === 'custom' && isset($assoc_args['directories'])) { + (new BlockAccessToSensitiveDirectories($assoc_args))->output(); + } } /** - * Blocks direct access to sensitive directories. + * Blocks author scanning * - * Blocks direct access to files in .git, svn and vendor directories + * Author scanning is a common technique of brute force attacks on WordPress. It is used to crack passwords for the known usernames and to gather + * additional information about the WordPress itself. * * ## OPTIONS * @@ -274,28 +272,24 @@ public function block_access_to_sensitive_files($args, $assoc_args): void { * * ## EXAMPLES * - * $ wp secure block_access_to_sensitive_directories - * Success: Block Access to Sensitive Directories rule has been deployed. + * $ wp secure block-author-scanning + * Success: Block Author Scanning rule has been deployed. * + * @subcommand block-author-scanning * @when before_wp_load */ - public function block_access_to_sensitive_directories($args, $assoc_args) : void { - (new BlockAccessToSensitiveDirectories($assoc_args))->output(); + public function block_author_scanning($args, $assoc_args) : void { + (new BlockAuthorScanning($assoc_args))->output(); } /** - * Blocks direct access to .htaccess + * Removes all WP CLI Secure rules * - * Blocks direct access to .htaccess file + * Use this command to remove all deployed security rules. If you are using nginx you need to restart it. + * If you copied rules manually, this command will not remove them! * * ## OPTIONS * - * [--remove] - * : Removes the rule from .htaccess or nginx.conf. - * - * [--output] - * : Use this option to display the actual code that you can manually copy and paste into some other file - * * [--file-path=] * : Set a custom path to the file which command should use to write rules into * @@ -305,121 +299,107 @@ public function block_access_to_sensitive_directories($args, $assoc_args) : void * * ## EXAMPLES * - * $ wp secure block_access_to_htaccess - * Success: Block Access to .htaccess rule has been deployed + * $ wp secure flush + * Success: All security rules have been removed. * * @when before_wp_load */ - public function block_access_to_htaccess($args, $assoc_args): void { - (new BlockAccessToHtaccess($assoc_args))->output(); + public function flush($args, $assoc_args) : void { + (new Flush($assoc_args))->output(); } /** - * Blocks author scanning - * - * Author scanning is a common technique of brute force attacks on WordPress. It is used to crack passwords for the known usernames and to gather - * additional information about the WordPress itself. - * - * ## OPTIONS + * Verifies WordPress files against WordPress.org's checksums. * - * [--remove] - * : Removes the rule from .htaccess or nginx.conf. + * Downloads md5 checksums for the current version from WordPress.org, and + * compares those checksums against the currently installed files. * - * [--output] - * : Use this option to display the actual code that you can manually copy and paste into some other file + * It also returns a list of files that shouldn't be part of default WordPress installation. * - * [--file-path=] - * : Set a custom path to the file which command should use to write rules into + * ## EXAMPLES * - * [--server=] - * : Set a server type. Possible options are "apache" and "nginx". Default is "apache" and all rules are stored in - * .htaccess file + * $ wp secure integrity-scan * - * ## EXAMPLES + * @param $args + * @param $assoc_args * - * $ wp secure block_author_scanning - * Success: Block Author Scanning rule has been deployed. + * @return void * + * @subcommand integrity-scan * @when before_wp_load */ - public function block_author_scanning($args, $assoc_args) : void { - (new BlockAuthorScanning($assoc_args))->output(); + public function integrity_scan($args, $assoc_args) : void { + WP_CLI::runcommand('core verify-checksums'); } /** - * Blocks access to XML-RPC - * - * XML-RPC is a remote procedure call which uses XML to encode its calls and HTTP as a transport mechanism. If you want to access and publish to your blog remotely, then you need XML-RPC enabled. - * For majority of WordPress installations, XML-RPC is not required and poses a significant security risk. + * Disable the file editor in WordPress * - * ## OPTIONS - * - * [--remove] - * : Removes the rule from .htaccess or nginx.conf. + * The problem with the WordPress file editor is that it allows users to run PHP code on your site. + * Anytime a user is able to run their own code, this presents a security risk. + * If an insecure admin account is hacked, the WordPress file editor is the gateway through which a full-fledged attack can be + * carried out. * - * [--output] - * : Use this option to display the actual code that you can manually copy and paste into some other file - * - * [--file-path=] - * : Set a custom path to the file which command should use to write rules into - * - * [--server=] - * : Set a server type. Possible options are "apache" and "nginx". Default is "apache" and all rules are stored in - * .htaccess file - * - * ## EXAMPLES - * - * $ wp secure block_author_scanning - * Success: Block Author Scanning rule has been deployed. + * @param $args + * @param $assoc_args * + * @subcommand disable-file-editor * @when before_wp_load + * + * @return void */ - public function block_access_to_xmlrpc($args, $assoc_args) : void { - (new BlockAccessToXmlRpc($assoc_args))->output(); + public function disable_file_editor($args, $assoc_args) : void { + WP_CLI::runcommand('config set DISALLOW_FILE_EDIT ' . !isset($assoc_args['remove'])); } - /** - * Removes all WP CLI Secure rules - * - * Use this command to remove all deployed security rules. If you are using nginx you need to restart it. If you copied rules manually, this command - * will not remove them! + /** + * Fix all directory and file permissions of the WordPress installation * - * ## OPTIONS - * - * [--file-path=] - * : Set a custom path to the file which command should use to write rules into + * Use this command to verify that the permissions of all files and directories are set according the WordPress recommendations. + * This command will set 0666 to all files and 0755 to all folders inside WordPress installation. * - * [--server=] - * : Set a server type. Possible options are "apache" and "nginx". Default is "apache" and all rules are stored in - * .htaccess file + * IMPORTANT: Don't use this command if you don't know what you are doing here! * * ## EXAMPLES * - * $ wp secure flush - * Success: All security rules have been removed. + * $ wp secure fix-permissions + * Success: All permission are set to the WordPress recommended values. * + * @subcommand fix-permissions * @when before_wp_load */ - public function flush($args, $assoc_args) : void { - (new Flush($assoc_args))->output(); + public function fix_permissions($args, $assoc_args) : void { + (new FixFileAndDirPermissions($assoc_args))->fixPermissions(); + + WP_CLI::success("Permission successfully updated."); } /** - * Verifies WordPress files against WordPress.org's checksums. + * Deploys all security rules at once * - * Downloads md5 checksums for the current version from WordPress.org, and - * compares those checksums against the currently installed files. + * This command will deploy all security rules at once. * - * It also returns a list of files that shouldn't be part of default WordPress installation. + * ## EXAMPLES + * + * $ wp secure all * * @param $args * @param $assoc_args * * @return void - * - * @when before_wp_load */ - public function integrityscan($args, $assoc_args) : void { - WP_CLI::runcommand('core verify-checksums'); + public function all($args, $assoc_args) : void { + (new DisableDirectoryBrowsing($assoc_args))->output(); + (new BlockPhpExecutionInPlugins($assoc_args))->output(); + (new BlockPhpExecutionInUploads($assoc_args))->output(); + (new BlockPhpExecutionInThemes($assoc_args))->output(); + (new BlockPhpExecutionInWpIncludes($assoc_args))->output(); + (new BlockAccessToXmlRpc($assoc_args))->output(); + (new BlockAccessToHtaccess($assoc_args))->output(); + (new BlockAccessToSensitiveFiles($assoc_args))->output(); + (new BlockAccessToSensitiveDirectories($assoc_args))->output(); + (new BlockAuthorScanning($assoc_args))->output(); + (new FixFileAndDirPermissions())->output(); + $this->disable_file_editor($args, $assoc_args); } } \ No newline at end of file diff --git a/src/SubCommands/BlockAccessToCustomSensitiveFiles.php b/src/SubCommands/BlockAccessToCustomSensitiveFiles.php new file mode 100644 index 0000000..1f6b85a --- /dev/null +++ b/src/SubCommands/BlockAccessToCustomSensitiveFiles.php @@ -0,0 +1,34 @@ +commandArguments['files']; + + if(!empty($files)) { + $files = explode(',', $files); + $files = array_map('trim', $files); + $files_array = []; + + foreach ($files as $key => $value) { + $file = (isset($this->commandArguments['server']) && $this->commandArguments['server'] === 'nginx') ? + preg_quote($value) : $value; + + $files_array[] = ['file' => $file]; + } + + return $files_array; + } + + return []; + } +} \ No newline at end of file diff --git a/src/SubCommands/BlockAccessToSensitiveDirectories.php b/src/SubCommands/BlockAccessToSensitiveDirectories.php index 7fea7dd..2533da6 100644 --- a/src/SubCommands/BlockAccessToSensitiveDirectories.php +++ b/src/SubCommands/BlockAccessToSensitiveDirectories.php @@ -7,14 +7,20 @@ class BlockAccessToSensitiveDirectories extends SubCommand { public string $ruleName = 'BLOCK ACCESS TO SENSITIVE DIRECTORIES'; public string $successMessage = 'Block Access to Sensitive Directories rule has been deployed.'; public string $removalMessage= 'Block Access to Sensitive Directories rule has been removed.'; + + /** + * @var string Default directories that we are going to protect + */ + private string $sensitiveDirectories = '.git,svn,vendors,cache'; + /** + * @return array + */ public function getTemplateVars() { - $directories = isset( $this->commandArguments['directories'] ) ? $this->commandArguments['directories'] : '.git,svn,vendors,cache'; + $directories = $this->commandArguments['directories'] ?? $this->sensitiveDirectories; if ( ! empty( $directories ) ) { $directories = explode( ',', $directories ); $directories = array_map( 'trim', $directories ); - $directories_array = []; - return [ [ 'directories' => implode( '|', array_map( 'preg_quote', $directories ) ) ] ]; diff --git a/src/SubCommands/BlockAccessToSensitiveFiles.php b/src/SubCommands/BlockAccessToSensitiveFiles.php index 1702696..f153ed4 100644 --- a/src/SubCommands/BlockAccessToSensitiveFiles.php +++ b/src/SubCommands/BlockAccessToSensitiveFiles.php @@ -28,7 +28,6 @@ public function getTemplateVars() { $files_array[] = [ 'file' => isset( $this->commandArguments['server'] ) && $this->commandArguments['server'] === 'nginx' ? preg_quote( $value ) : $value ]; } } - return $files_array; } return []; diff --git a/src/SubCommands/BlockPhpExecutionInWpIncludes.php b/src/SubCommands/BlockPhpExecutionInWpIncludes.php index 81c0fa2..8f3eb61 100644 --- a/src/SubCommands/BlockPhpExecutionInWpIncludes.php +++ b/src/SubCommands/BlockPhpExecutionInWpIncludes.php @@ -4,7 +4,7 @@ class BlockPhpExecutionInWpIncludes extends SubCommand { public string $ruleTemplate = 'block_php_execution_in_wp_includes'; - public string $ruleName = 'BLOCK PHP EXECUTION IN UPLOADS'; + public string $ruleName = 'BLOCK PHP EXECUTION IN WP INCLUDES'; public string $successMessage = 'Block Execution In wp-includes Directory rule has been deployed.'; public string $removalMessage= 'Block Execution In wp-includes Directory rule has been removed.'; } \ No newline at end of file diff --git a/src/SubCommands/FixFileAndDirPermissions.php b/src/SubCommands/FixFileAndDirPermissions.php new file mode 100644 index 0000000..257def0 --- /dev/null +++ b/src/SubCommands/FixFileAndDirPermissions.php @@ -0,0 +1,34 @@ +filePermissions : $this->directoryPermissions); + } + + return true; + } +} \ No newline at end of file diff --git a/src/SubCommands/SubCommand.php b/src/SubCommands/SubCommand.php index fd09232..d188df9 100644 --- a/src/SubCommands/SubCommand.php +++ b/src/SubCommands/SubCommand.php @@ -34,7 +34,7 @@ class SubCommand { /** * @var mixed The content of the rule template file */ - public mixed $ruleContent; + public $ruleContent; /** * @var string Rule template name @@ -99,7 +99,7 @@ private function setFilePath() : string { } /** - * Reads rule template file. Depending on output type, returns string or an array + * Reads rule template file. Depending on output type, returns an array * * @param boolean $loadVars Whether to load the template vars or not. * @param boolean $template Template name to return instead of the loaded one. @@ -125,6 +125,8 @@ protected function setRuleContent( bool $loadVars = true, bool|string $template unset($file); if ( $loadVars ) { + //Combine templates and command arguments, if any + //This is used for block-access command $result = new RuleContent( $result, $this->getTemplateVars() ); $result = $result->getContent(); } @@ -137,7 +139,7 @@ protected function setRuleContent( bool $loadVars = true, bool|string $template * * @return array */ - public function getTemplateVars() { + public function getTemplateVars(): array { return []; } @@ -166,34 +168,31 @@ private function getOutputMessage(string $type = 'success') : string { * @throws WP_CLI\ExitException */ public function output() { - if($this->output) { - try { - $fileManager = new FileManager($this->filePath); - $content = $fileManager->wrap($this->ruleContent, 'block', $this->ruleName); - WP_CLI::line(implode(PHP_EOL, $content)); - } catch(FileDoesNotExist|RuleAlreadyExist|FileIsNotWritable|FileIsNotReadable $e) { - WP_CLI::error($e->getMessage()); - } - } else { - try { - $fileManager = new FileManager($this->filePath); - - if(isset($this->commandArguments['remove']) && $this->commandArguments['remove'] === true) { - //We need to remove the rule from file - $result = $fileManager->remove($this->ruleName); - - if($result) { - WP_CLI::success($this->getOutputMessage('removal')); - } - } else { - //Add the rule - $fileManager->add($this->ruleContent, $this->ruleName); - - WP_CLI::success($this->getOutputMessage('success')); - } - } catch(FileDoesNotExist|RuleAlreadyExist|FileIsNotWritable|FileIsNotReadable $e) { - WP_CLI::error($e->getMessage()); - } - } + try { + $fileManager = new FileManager($this->filePath); + if ($this->output) { + $content = $fileManager->wrap($this->ruleContent, 'block', $this->ruleName); + WP_CLI::line( implode( PHP_EOL, $content ) ); + } else { + if (isset($this->commandArguments['remove']) && $this->commandArguments['remove'] === true) { + //We need to remove the rule from file + $result = $fileManager->remove($this->ruleName); + + if ($result) { + WP_CLI::success($this->getOutputMessage('removal')); + } + } else { + //Add the rule + $fileManager->add($this->ruleContent, $this->ruleName); + + WP_CLI::success($this->getOutputMessage('success')); + } + + } + } catch (FileDoesNotExist | FileIsNotWritable | FileIsNotReadable $e) { + WP_CLI::error($e->getMessage()); + } catch (RuleAlreadyExist $e) { + WP_CLI::warning($e->getMessage()); + } } -} \ No newline at end of file +} diff --git a/src/Templates/apache/block_access_to_custom_sensitive_files.tpl b/src/Templates/apache/block_access_to_custom_sensitive_files.tpl new file mode 100644 index 0000000..f38d322 --- /dev/null +++ b/src/Templates/apache/block_access_to_custom_sensitive_files.tpl @@ -0,0 +1,9 @@ + + + Require all denied + + + Order allow,deny + Deny from all + + diff --git a/src/Templates/apache/block_access_to_sensitive_files.tpl b/src/Templates/apache/block_access_to_sensitive_files.tpl index f38d322..16d2b40 100644 --- a/src/Templates/apache/block_access_to_sensitive_files.tpl +++ b/src/Templates/apache/block_access_to_sensitive_files.tpl @@ -1,4 +1,4 @@ - + Require all denied @@ -7,3 +7,26 @@ Deny from all + + + Require all denied + + + Order allow,deny + Deny from all + + + + + Require all denied + + + Order allow,deny + Deny from all + + + + RewriteEngine On + RewriteRule ^wp-admin/install\.php$ - [F] + RewriteRule ^wp-admin/upgrade\.php$ - [F] + \ No newline at end of file diff --git a/src/Templates/nginx/block_access_to_custom_sensitive_files.tpl b/src/Templates/nginx/block_access_to_custom_sensitive_files.tpl new file mode 100644 index 0000000..522e048 --- /dev/null +++ b/src/Templates/nginx/block_access_to_custom_sensitive_files.tpl @@ -0,0 +1,3 @@ +location ~ /{{file}}$ { + deny all; +} diff --git a/src/Templates/nginx/block_access_to_sensitive_files.tpl b/src/Templates/nginx/block_access_to_sensitive_files.tpl index 522e048..7f75180 100644 --- a/src/Templates/nginx/block_access_to_sensitive_files.tpl +++ b/src/Templates/nginx/block_access_to_sensitive_files.tpl @@ -1,3 +1,19 @@ -location ~ /{{file}}$ { +location = /wp-admin/install.php { deny all; } + +location = /wp-admin/upgrade.php { + deny all; +} + +location ~ /readme\.html$ { + deny all; +} + +location ~ /readme\.txt$ { + deny all; +} + +location ~ /wp-config.php$ { + deny all; +} \ No newline at end of file diff --git a/tests/Feature/BlockAccessToHtaccessTest.php b/tests/Feature/BlockAccessToHtaccessTest.php index 95ca187..0a5be8a 100644 --- a/tests/Feature/BlockAccessToHtaccessTest.php +++ b/tests/Feature/BlockAccessToHtaccessTest.php @@ -15,7 +15,7 @@ public function setUp(): void{ $command = new BlockAccessToHtaccess($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingNginxConfigFile() : void { diff --git a/tests/Feature/BlockAccessToSensitiveDirectoriesTest.php b/tests/Feature/BlockAccessToSensitiveDirectoriesTest.php index f2afc5a..1189a18 100644 --- a/tests/Feature/BlockAccessToSensitiveDirectoriesTest.php +++ b/tests/Feature/BlockAccessToSensitiveDirectoriesTest.php @@ -15,7 +15,7 @@ public function setUp(): void{ $command = new BlockAccessToSensitiveDirectories($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingGitDirectory() : void { diff --git a/tests/Feature/BlockAccessToSensitiveFilesTest.php b/tests/Feature/BlockAccessToSensitiveFilesTest.php index 38f68bb..246f6dc 100644 --- a/tests/Feature/BlockAccessToSensitiveFilesTest.php +++ b/tests/Feature/BlockAccessToSensitiveFilesTest.php @@ -15,7 +15,7 @@ public function setUp(): void { $command = new BlockAccessToSensitiveFiles($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingReadmeFiles() : void { diff --git a/tests/Feature/BlockAccessToXmlRpcTest.php b/tests/Feature/BlockAccessToXmlRpcTest.php index 2c917bb..a102e4a 100644 --- a/tests/Feature/BlockAccessToXmlRpcTest.php +++ b/tests/Feature/BlockAccessToXmlRpcTest.php @@ -15,7 +15,7 @@ public function setUp(): void { $command = new BlockAccessToXmlRpc($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingXmlRpcFile() : void { diff --git a/tests/Feature/BlockAuthorScanningTest.php b/tests/Feature/BlockAuthorScanningTest.php index 6867bac..c9cedb0 100644 --- a/tests/Feature/BlockAuthorScanningTest.php +++ b/tests/Feature/BlockAuthorScanningTest.php @@ -12,7 +12,7 @@ public function setUp(): void { $command = new BlockAuthorScanning($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnApacheWhenAccessingPhpFilesInWpIncludesDirectory() : void { diff --git a/tests/Feature/BlockPhpExecutionInPluginsTest.php b/tests/Feature/BlockPhpExecutionInPluginsTest.php index cb1a5d0..5cea692 100644 --- a/tests/Feature/BlockPhpExecutionInPluginsTest.php +++ b/tests/Feature/BlockPhpExecutionInPluginsTest.php @@ -15,7 +15,7 @@ public function setUp(): void { $command = new BlockPhpExecutionInPlugins($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingPhpFilesInPlugins() : void { diff --git a/tests/Feature/BlockPhpExecutionInThemesTest.php b/tests/Feature/BlockPhpExecutionInThemesTest.php index 001f962..c6ba7dc 100644 --- a/tests/Feature/BlockPhpExecutionInThemesTest.php +++ b/tests/Feature/BlockPhpExecutionInThemesTest.php @@ -15,7 +15,7 @@ public function setUp(): void { $command = new BlockPhpExecutionInThemes($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingPhpFilesInThemesDirectory() : void { diff --git a/tests/Feature/BlockPhpExecutionInUploadsTest.php b/tests/Feature/BlockPhpExecutionInUploadsTest.php index bdaca27..db99cde 100644 --- a/tests/Feature/BlockPhpExecutionInUploadsTest.php +++ b/tests/Feature/BlockPhpExecutionInUploadsTest.php @@ -15,7 +15,7 @@ public function setUp(): void { $command = new BlockPhpExecutionInUploads($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingPhpFilesInUploadsDirectory() : void { diff --git a/tests/Feature/BlockPhpExecutionInWpIncludesTest.php b/tests/Feature/BlockPhpExecutionInWpIncludesTest.php index 7576917..20ce4c5 100644 --- a/tests/Feature/BlockPhpExecutionInWpIncludesTest.php +++ b/tests/Feature/BlockPhpExecutionInWpIncludesTest.php @@ -15,7 +15,7 @@ public function setUp(): void { $command = new BlockPhpExecutionInWpIncludes($this->apacheAssocArgs); $command->output(); - exec('cd ' . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); + exec('cd ' . getcwd() . DIRECTORY_SEPARATOR . $_ENV['WORDPRESS_NGINX_PATH'] . '&& ddev exec nginx -s reload'); } public function testItWillReturnHttp403OnNginxWhenAccessingPhpFilesInWpIncludesDirectory() : void { diff --git a/tests/Unit/FileManager/WriteTest.php b/tests/Unit/FileManager/WriteTest.php index ec6b18e..dc2bcf7 100644 --- a/tests/Unit/FileManager/WriteTest.php +++ b/tests/Unit/FileManager/WriteTest.php @@ -41,8 +41,8 @@ class WriteTest extends BaseTestCase { public function setUp(): void { parent::setUp(); - $content = file_get_contents(dirname(__DIR__) . '/assets/htaccess-base.txt'); - $content2 = file_get_contents(dirname(__DIR__) . '/assets/htaccess-secured.txt'); + $content = file_get_contents(getcwd() . '/tests/assets/htaccess-base.txt'); + $content2 = file_get_contents(getcwd() . '/tests/assets/htaccess-secured.txt'); $this->file = FileHelper::create('.htaccess', 0755, $content); $this->file2 = FileHelper::create('.htaccess2', 0777, $content); $this->file3 = FileHelper::create('.htaccess-secured', 0666, $content2);