Skip to content

Commit

Permalink
Merge branch 'release-0.11'
Browse files Browse the repository at this point in the history
  • Loading branch information
chesio committed Jan 28, 2019
2 parents 86655be + 0c12332 commit 88b5fe7
Show file tree
Hide file tree
Showing 25 changed files with 510 additions and 110 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ BC Security allows you to:
1. Disable pingbacks
1. Disable XML RPC methods that require authentication
1. Disable access to REST API to anonymous users
1. Check and/or validate user passwords using [Pwned Passwords](https://haveibeenpwned.com/Passwords) database and [API](https://haveibeenpwned.com/API/v2#PwnedPasswords)

#### Passwords check

Passwords are checked on user login. If password is present in the Pwned Passwords database, a non-dismissible warning is displayed in backend encouraging the user to change its password. By default, the warning is displayed on all pages, but this can be [customized via a filter](#customization).

#### Passwords validation

Passwords are validated on user creation, password change or password reset. If password is present in the Pwned Passwords database, the operation is aborted with an error message asking user to pick a different password.

### Login security

Expand Down Expand Up @@ -111,11 +120,13 @@ Logs are stored in database and can be viewed on backend. Logs are automatically
Some of the modules listed above come with settings panel. Further customization can be done with filters provided by plugin:

* `bc-security/filter:is-admin` - filters boolean value that determines whether current user is considered an admin user. This check determines whether admin login notification should be sent for particular user. By default, any user with `manage_options` capability is considered an admin (or `manage_network` on multisite).
* `bc-security/filter:plugin-changelog-url` - filters changelog URL of given plugin. Might come handy in case of plugins not hosted in Plugins Directory.
* `bc-security/filter:obvious-usernames` - filters array of common usernames that are being checked via [checklist check](#basic-checks). By default, the array consists of _admin_ and _administrator_ values.
* `bc-security/filter:plugins-to-check-for-integrity` - filters array of plugins that should have their integrity checked. By default, the array consists of all installed plugins that have _readme.txt_ file. Note that plugins under version control are automatically omitted.
* `bc-security/filter:plugins-to-check-for-removal` - filters array of plugins to check for their presence in WordPress.org Plugins Directory. By default, the array consists of all installed plugins that have _readme.txt_ file.
* `bc-security/filter:modified-files-ignored-in-core-integrity-check` - filters array of files that should not be reported as __modified__ in checksum verification of core WordPress files. By default, the array consist of _wp-config-sample.php_ and _wp-includes/version.php_ values.
* `bc-security/filter:unknown-files-ignored-in-core-integrity-check` - filters array of files that should not be reported as __unknown__ in checksum verification of core WordPress files. By default, the array consist of _.htaccess_, _wp-config.php_, _liesmich.html_, _olvasdel.html_ and _procitajme.html_ values.
* `bc-security/filter:show-pwned-password-warning` - filters whether the ["pwned password" warning](#passwords-check) should be displayed for current user on current screen.
* `bc-security/filter:ip-blacklist-default-manual-lock-duration` - filters number of seconds that is used as default value in lock duration field of manual IP blacklisting form. By default, the value is equal to one month in seconds.
* `bc-security/filter:is-ip-address-locked` - filters boolean value that determines whether given IP address is currently locked within given scope. By default, the value is based on plugin bookkeeping data.
* `bc-security/filter:log-404-event` - filters boolean value that determines whether current HTTP request that resulted in [404 response](https://en.wikipedia.org/wiki/HTTP_404) should be logged or not. To completely disable logging of 404 events, you can attach [`__return_false`](https://developer.wordpress.org/reference/functions/__return_false/) function to the filter.
Expand All @@ -125,10 +136,11 @@ Some of the modules listed above come with settings panel. Further customization
## Credits

1. [Login Security](#login-security) feature is inspired by [Limit Login Attempts](https://wordpress.org/plugins/limit-login-attempts/) plugin by Johan Eenfeldt.
1. Part of [psr/log](https://packagist.org/packages/psr/log) package codebase is shipped with the plugin.
1. [WordPress core integrity check](#wordpress-core-integrity-check) is heavily inspired by [Checksum Verifier](https://github.com/pluginkollektiv/checksum-verifier) plugin by Sergej Müller.
1. Some features (like "[Removed plugins check](#removed-plugins-check)") are inspired by [Wordfence Security](https://wordpress.org/plugins/wordfence/) from [Defiant](https://www.defiant.com/).
1. [Passwords check](#passwords-check) and [passwords validation](#passwords-validation) features uses API and data made available by [Have I Been Pwned](https://haveibeenpwned.com) project by [Troy Hunt](https://www.troyhunt.com).
1. Big thanks to [Vincent Driessen](https://nvie.com/about/) for his "[A successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model/)" article that I find particularly useful every time I do some work on BC Security.
1. Part of [psr/log](https://packagist.org/packages/psr/log) package codebase is shipped with the plugin.

## Alternatives (and why I do not use them)

Expand Down
12 changes: 10 additions & 2 deletions bc-security.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: BC Security
* Plugin URI: https://github.com/chesio/bc-security
* Description: Helps keeping WordPress websites secure.
* Version: 0.10.0
* Version: 0.11.0
* Author: Česlav Przywara <[email protected]>
* Author URI: https://www.chesio.com
* Requires PHP: 7.1
Expand All @@ -16,14 +16,22 @@
if (version_compare(PHP_VERSION, '7.1', '<')) {
// Warn user that his/her PHP version is too low for this plugin to function.
add_action('admin_notices', function () {
echo '<div class="error"><p>';
echo '<div class="notice notice-error"><p>';
echo esc_html(
sprintf(
__('BC Security plugin requires PHP 7.1 to function properly, but you have version %s installed. The plugin has been auto-deactivated.', 'bc-security'),
PHP_VERSION
)
);
echo '</p></div>';
// Warn user that his/her PHP version is no longer supported.
echo '<div class="notice notice-warning"><p>';
echo sprintf(
__('PHP version %1$s is <a href="%2$s">no longer supported</a>. You should consider upgrading PHP on your webhost.', 'bc-security'),
PHP_VERSION,
'https://secure.php.net/supported-versions.php'
);
echo '</p></div>';
// https://make.wordpress.org/plugins/2015/06/05/policy-on-php-versions/
if (isset($_GET['activate'])) {
unset($_GET['activate']);
Expand Down
4 changes: 2 additions & 2 deletions classes/BlueChip/Security/Core/Admin/SettingsPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ protected function getFieldBaseProperties(string $key, $value = null): array
*
* @param string $section
* @param string $title
* @param callable $callback
* @param callable|null $callback [optional]
*/
public function addSettingsSection(string $section, string $title, $callback = null)
public function addSettingsSection(string $section, string $title, ?callable $callback = null)
{
if (!is_string($this->recent_page)) {
_doing_it_wrong(__METHOD__, 'No recent page set yet!', '0.1.0');
Expand Down
63 changes: 63 additions & 0 deletions classes/BlueChip/Security/Helpers/HaveIBeenPwned.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* @package BC_Security
*/
namespace BlueChip\Security\Helpers;

/**
* @link https://haveibeenpwned.com/
*/
abstract class HaveIBeenPwned
{
/**
* @var string URL of Pwned Passwords home page
*/
const PWNEDPASSWORDS_HOME_URL = 'https://haveibeenpwned.com/Passwords';

/**
* @link https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange
* @var string URL of Pwned Passwords API range search end-point
*/
const PWNEDPASSWORDS_API_RANGE_SEARCH_URL = 'https://api.pwnedpasswords.com/range/';


/**
* @link https://haveibeenpwned.com/API/v2#PwnedPasswords
* @param string $password Password to check.
* @return bool True, if $password has been previously exposed in a data breach, false if not, null if check failed.
*/
public static function hasPasswordBeenPwned(string $password): ?bool
{
$sha1 = sha1($password);

// Only first 5 characters of the hash are required.
$sha1_prefix = substr($sha1, 0, 5);

$response = wp_remote_get(esc_url(self::PWNEDPASSWORDS_API_RANGE_SEARCH_URL . $sha1_prefix));

if (wp_remote_retrieve_response_code($response) !== 200) {
// Note: "there is no circumstance in which the API should return HTTP 404",
// but of course remote request can always fail due network issues.
return null;
}

$body = wp_remote_retrieve_body($response);
if (empty($body)) {
// Note: Should never happen, as there is a non-empty response for every prefix,
// therefore return null (check failed) rather than false (check negative).
return null;
}

// Every record has "hash_suffix:count" format.
$records = explode(PHP_EOL, $body);
foreach ($records as $record) {
[$sha1_suffix, $count] = explode(':', $record);

if ($sha1 === ($sha1_prefix . strtolower($sha1_suffix))) {
return true; // Your password been pwned, my friend!
}
}

return false; // Ok, you're fine.
}
}
4 changes: 2 additions & 2 deletions classes/BlueChip/Security/Modules/Checklist/CheckResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class CheckResult
* @param bool|null $status Check result status: false, if check failed; true, if check passed; null for undetermined status.
* @param array|string $message Human readable message explaining the result - inline HTML tags are allowed/expected.
*/
public function __construct($status, $message)
public function __construct(?bool $status, $message)
{
$this->status = $status;
$this->message = is_array($message) ? $message : [$message];
Expand Down Expand Up @@ -56,7 +56,7 @@ public function getMessageAsPlainText(): string
/**
* @return bool|null Check result status: false, if check failed; true, if check passed; null means status is undetermined.
*/
public function getStatus()
public function getStatus(): ?bool
{
return $this->status;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,12 @@ protected function runInternal(): Checklist\CheckResult


/**
* @return string URL to checksums file at api.wordpress.org for current WordPress version and locale.
* @return string URL to checksums file at api.wordpress.org for current WordPress version.
*/
public static function getChecksumsUrl(): string
{
// Add necessary arguments to request URL.
return add_query_arg(
[
'version' => get_bloginfo('version'),
'locale' => get_locale(), // TODO: What about multilanguage sites?
],
self::CHECKSUMS_API_URL
);
// Add version number to request URL.
return add_query_arg('version', get_bloginfo('version'), self::CHECKSUMS_API_URL);
}


Expand All @@ -100,7 +94,10 @@ private static function getChecksums(string $url)
{
$json = Checklist\Helper::getJson($url);

return $json && !empty($json->checksums) ? $json->checksums : null;
// When no locale is specified in API request, checksums are stored under additional version number key.
$version = get_bloginfo('version');

return $json && !empty($json->checksums) && !empty($json->checksums->$version) ? $json->checksums->$version : null;
}


Expand All @@ -110,6 +107,9 @@ private static function getChecksums(string $url)
* Files in wp-content directory are automatically excluded, see:
* https://github.com/pluginkollektiv/checksum-verifier/pull/11
*
* Some files are ignored automatically, because they may differ between localized version of WordPress, see:
* https://meta.trac.wordpress.org/ticket/4008
*
* @hook \BlueChip\Security\Modules\Checklist\Hooks::IGNORED_CORE_MODIFIED_FILES
*
* @param \stdClass $checksums
Expand All @@ -121,6 +121,7 @@ private static function findModifiedFiles($checksums): array
$ignored_files = apply_filters(
Checklist\Hooks::IGNORED_CORE_MODIFIED_FILES,
[
'readme.html',
'wp-config-sample.php',
'wp-includes/version.php',
]
Expand Down Expand Up @@ -170,7 +171,7 @@ private static function findUnknownFiles($checksums): array
// Scan wp-include directory recursively.
Checklist\Helper::scanDirectoryForUnknownFiles(ABSPATH . WPINC, ABSPATH, $checksums, true)
),
function ($filename) use ($ignored_files) {
function (string $filename) use ($ignored_files): bool {
return !in_array($filename, $ignored_files, true);
}
);
Expand Down
6 changes: 3 additions & 3 deletions classes/BlueChip/Security/Modules/Checklist/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ function (string $file): string {
* is returned. Null is also returned for HTTP status 200 if response body is different than $body (if given).
*
* @param string $url URL to check.
* @param string $body Response body to check [optional].
* @return null|bool
* @param string|null $body Response body to check [optional].
* @return bool|null
*/
public static function isAccessToUrlForbidden(string $url, $body = null)
public static function isAccessToUrlForbidden(string $url, ?string $body = null): ?bool
{
// Try to get provided URL. Use HEAD request for simplicity, if response body is of no interest.
$response = is_string($body) ? wp_remote_get($url) : wp_remote_head($url);
Expand Down
32 changes: 32 additions & 0 deletions classes/BlueChip/Security/Modules/Hardening/AdminPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace BlueChip\Security\Modules\Hardening;

use BlueChip\Security\Helpers\FormHelper;
use BlueChip\Security\Helpers\HaveIBeenPwned;

class AdminPage extends \BlueChip\Security\Core\Admin\AbstractPage
{
Expand Down Expand Up @@ -112,5 +113,36 @@ function () {
__('Disable REST API access', 'bc-security'),
[FormHelper::class, 'printCheckbox']
);

// Section: Check/validate user passwords against Pwned Passwords database
$this->addSettingsSection(
'pwned-passwords',
__('Validate user passwords against Pwned Passwords database', 'bc-security'),
function () {
echo '<p>' . sprintf(
__('<a href="%1$s">Pwned Passwords</a> is a large database of passwords previously exposed in data breaches. This exposure makes them unsuitable for ongoing use as they are at much greater risk of being used to take over other accounts.', 'bc-security'),
HaveIBeenPwned::PWNEDPASSWORDS_HOME_URL
) . '</p>';
echo '<p>' . __('BC Security allows you to utilize this database in two ways:', 'bc-security');
echo '<ol>';
echo '<li>' . __("When <strong>password validation</strong> is enabled, passwords are checked against the Pwned Passwords database when new user is being created or existing user's password is being changed via profile update page or through password reset form. If there is a match, the operation is aborted with an error message asking for a different password.", 'bc-security') . '</li>';
echo '<li>' . __("When <strong>password check</strong> is enabled, passwords are checked against the Pwned Passwords database when user logs in to the backend. If there is a match, a non-dismissible warning is displayed on all back-end pages encouraging the user to change its password.", 'bc-security') . '</li>';
echo '</ol>';
echo '<p>' . sprintf(
__('Important: Only the first 5 characters of SHA-1 hash of the actual password are ever shared with Pwned Passwords service. See <a href="%s">Pwned Passwords API documentation</a> for more details.', 'bc-security'),
'https://haveibeenpwned.com/API/v2#PwnedPasswords'
) . '</p>';
}
);
$this->addSettingsField(
Settings::VALIDATE_PASSWORDS,
__('Validate passwords on user creation or password change', 'bc-security'),
[FormHelper::class, 'printCheckbox']
);
$this->addSettingsField(
Settings::CHECK_PASSWORDS,
__('Check passwords of existing users', 'bc-security'),
[FormHelper::class, 'printCheckbox']
);
}
}
Loading

0 comments on commit 88b5fe7

Please sign in to comment.