Skip to content

Commit

Permalink
Merge branch 'release-0.23.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
chesio committed Apr 4, 2024
2 parents b903c28 + cf8d413 commit 0122d94
Show file tree
Hide file tree
Showing 66 changed files with 1,001 additions and 897 deletions.
24 changes: 12 additions & 12 deletions .github/workflows/integrate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"

- name: "Check file permissions"
run: |
Expand All @@ -57,7 +57,7 @@ jobs:
uses: "actions/checkout@v4"

- name: "Install dependencies"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"

Expand Down Expand Up @@ -88,24 +88,24 @@ jobs:
extensions: "mbstring"

- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"

- name: "Install dependencies"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "${{ matrix.dependencies }}"

- name: "Raise constraint for antecedent/patchwork"
if: "${{ matrix.dependencies == 'lowest' }}"
run: "composer require --dev --prefer-lowest --update-with-all-dependencies 'antecedent/patchwork:^2.0.8'"
run: "composer require --dev --prefer-lowest --update-with-all-dependencies 'antecedent/patchwork:^2.1.26'"

- name: "Execute unit tests"
if: "${{ ! (matrix.php-version == '8.1' && matrix.dependencies == 'highest') }}"
run: "composer run-script unit-tests -- --no-coverage"
run: "composer run-script unit-tests"

- name: "Execute unit tests with coverage"
if: "${{ matrix.php-version == '8.1' && matrix.dependencies == 'highest' }}"
run: "composer run-script unit-tests"
run: "composer run-script unit-tests-with-coverage"

- name: "Send coverage to Coveralls"
env:
Expand All @@ -130,13 +130,13 @@ jobs:
coverage: "none"

- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"

- name: "Validate Composer configuration"
run: "composer validate --strict"

- name: "Install dependencies"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"

Expand All @@ -161,7 +161,7 @@ jobs:
coverage: "none"

- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"

- name: "Check EditorConfig configuration"
run: "test -f .editorconfig"
Expand All @@ -170,7 +170,7 @@ jobs:
uses: "greut/eclint-action@v0"

- name: "Install dependencies"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"

Expand All @@ -185,7 +185,7 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- name: "Checkout code"
uses: "actions/checkout@v3"
uses: "actions/checkout@v4"

- name: "Check exported files"
run: |
Expand Down
22 changes: 20 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# BC Security Changelog

## Version 0.23.0 (2024-04-04)

**Important**: either deactivate and reactivate plugin after update or install new cron job manually via WP-CLI: `wp cron event schedule bc-security/failed-logins-clean-up now daily`.

### Added

* New built-in rule for bad request banner module that triggers when non-existing `.tgz` or `.zip` file is accessed [#155](https://github.com/chesio/bc-security/issues/155).
* Plugin has been tested with WordPress 6.5 [#152](https://github.com/chesio/bc-security/issues/152).

### Changed

* List of supported PHP versions for PHP version check has been updated to include PHP 8.3 [#151](https://github.com/chesio/bc-security/issues/151).

### Fixed

* Fix SQL syntax error when bulk unlocking entries in internal blocklist [#154](https://github.com/chesio/bc-security/pull/154) - thanks to @szepeviktor.
* Table storing failed logins data is now pruned automatically [#156](https://github.com/chesio/bc-security/issues/156).

## Version 0.22.1 (2024-02-07)

### Fixed
Expand All @@ -12,7 +30,7 @@ This release has been tested with PHP 8.3 and WordPress 6.4. PHP 8.1 or newer an

### Added

* New built-in rule to bad request banner module that triggers when non-existing `readme.txt` file is accessed [#149](https://github.com/chesio/bc-security/issues/149).
* New built-in rule for bad request banner module that triggers when non-existing `readme.txt` file is accessed [#149](https://github.com/chesio/bc-security/issues/149).
* Plugin has been tested with PHP 8.3 [#145](https://github.com/chesio/bc-security/issues/145).
* Plugin has been tested with WordPress 6.4 [#144](https://github.com/chesio/bc-security/issues/144).

Expand Down Expand Up @@ -92,7 +110,7 @@ These adjustments led to some breaking changes, therefore during update it is re
* PHP 8.0 is supported [#104](https://github.com/chesio/bc-security/issues/104).
* Alert about "No removed plugins installed" has more information [#107](https://github.com/chesio/bc-security/issues/107).
* Detection of plugins installed from WordPress Directory has been improved [#112](https://github.com/chesio/bc-security/issues/112).
* On WordPress 5.8 and newer the plugin cannot be accidentally overriden from WordPress.org Plugins Directory [#111](https://github.com/chesio/bc-security/issues/111).
* On WordPress 5.8 and newer the plugin cannot be accidentally overridden from WordPress.org Plugins Directory [#111](https://github.com/chesio/bc-security/issues/111).

## Older releases

Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ You may also optionally provide Google API key if you want to check your website

BC Security can help you find potential security issues or even signs of breach.

Since security measures for development instalations do not have to be as strict as for live installations, some checks are run only in *live environment*. A *live environment* is determined as one where [`wp_get_environment_type()`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) returns either `production` or `staging`, but there is a [dedicated filter](#customization) that can be used to override *live environment* detection.
Since security measures for development installations do not have to be as strict as for live installations, some checks are run only in *live environment*. A *live environment* is determined as one where [`wp_get_environment_type()`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) returns either `production` or `staging`, but there is a [dedicated filter](#customization) that can be used to override *live environment* detection.

#### Basic checks

Expand Down Expand Up @@ -144,10 +144,11 @@ Passwords are validated on user creation, password change or password reset. If

Remote IP addresses that are scanning your website for weaknesses can be automatically [blocked](#internal-blocklist) for configured amount of time. Such scanners can be usually quite easily detected because while scanning a website they trigger a lot of 404 errors and URLs they try to access differ from "valid" 404 errors: usually they try to find a known vulnerable plugin, forgotten backup file or PHP script used for administrative purposes.

There are three built-in rules available (they are not active by default):
There are four built-in rules available (they are not active by default):
1. ban when non-existent PHP file is requested (any URL ending with `.php`)
2. ban when non-existent backup file is requested (any URL targeting file with `backup` in basename or with `.back`, `.old` or `.tmp` extension)
3. ban when non-existent `readme.txt` file is accessed
2. ban when non-existent archive file is requested (any URL ending with `.tgz` or `.zip`)
3. ban when non-existent backup file is requested (any URL targeting file with `backup` in basename or with `.back`, `.old` or `.tmp` extension)
4. ban when non-existent `readme.txt` file is accessed

You may define custom rules as well (in form of regular expression).

Expand Down Expand Up @@ -198,7 +199,10 @@ You can mute all email notifications by setting constant `BC_SECURITY_MUTE_NOTIF
Following events triggered by BC Security are logged:

1. Short and long lockout events (see [Login Security](#login-security) feature)
2. Requests blocked by [external](#external-blocklist) or [internal](#internal-blocklist) blocklist
2. Requests blocked by [external](#external-blocklist) or [internal](#internal-blocklist) blocklist _(* see note below)_
3. Requests that match any of configured [bad request rules](#bad-requests-banner)

_(*) Note: in case internal blocklist is synchronized with `.htaccess` file, HTTP requests are blocked by webserver before being handled to WordPress, therefore they cannot be logged by the plugin._

Following events triggered by WordPress core are logged:

Expand Down
4 changes: 2 additions & 2 deletions bc-security.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
* Plugin Name: BC Security
* Plugin URI: https://github.com/chesio/bc-security
* Description: Helps keeping WordPress websites secure.
* Version: 0.22.1
* Version: 0.23.0
* Author: Česlav Przywara <[email protected]>
* Author URI: https://www.chesio.com
* Requires PHP: 8.1
* Requires at least: 6.2
* Tested up to: 6.4
* Tested up to: 6.5
* Text Domain: bc-security
* GitHub Plugin URI: https://github.com/chesio/bc-security
* Update URI: https://github.com/chesio/bc-security
Expand Down
14 changes: 7 additions & 7 deletions classes/BlueChip/Security/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ class Admin
*/
public function init(string $plugin_filename): self
{
add_action('admin_menu', [$this, 'makeAdminMenu']);
add_action('admin_init', [$this, 'initAdminPages']);
add_filter('plugin_action_links_' . plugin_basename($plugin_filename), [$this, 'filterActionLinks']);
add_action('admin_menu', $this->makeAdminMenu(...));
add_action('admin_init', $this->initAdminPages(...));
add_filter('plugin_action_links_' . plugin_basename($plugin_filename), $this->filterActionLinks(...));
return $this;
}

Expand All @@ -59,7 +59,7 @@ public function addPage(Core\Admin\AbstractPage $page): self
/**
* @action https://developer.wordpress.org/reference/hooks/admin_init/
*/
public function initAdminPages(): void
private function initAdminPages(): void
{
foreach ($this->pages as $page) {
$page->initPage();
Expand All @@ -72,7 +72,7 @@ public function initAdminPages(): void
*
* @action https://developer.wordpress.org/reference/hooks/admin_menu/
*/
public function makeAdminMenu(): void
private function makeAdminMenu(): void
{
if (empty($this->pages)) {
// No pages registered = no pages (no menu) to show.
Expand Down Expand Up @@ -100,7 +100,7 @@ public function makeAdminMenu(): void
$page->getMenuTitle() . $this->renderCounter($page),
self::CAPABILITY,
$page->getSlug(),
[$page, 'printContents']
$page->printContents(...),
);
if ($page_hook) {
$page->setPageHook($page_hook);
Expand All @@ -118,7 +118,7 @@ public function makeAdminMenu(): void
*
* @return string[]
*/
public function filterActionLinks(array $links): array
private function filterActionLinks(array $links): array
{
if (current_user_can(self::CAPABILITY) && isset($this->pages['bc-security-setup'])) {
$links[] = \sprintf(
Expand Down
4 changes: 2 additions & 2 deletions classes/BlueChip/Security/Core/Admin/AbstractPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static function getPageUrl(): string
*/
public function setPageHook(string $page_hook): void
{
add_action('load-' . $page_hook, [$this, 'loadPage']);
add_action('load-' . $page_hook, $this->loadPage(...));
}


Expand All @@ -109,7 +109,7 @@ public function initPage(): void
*
* @action https://developer.wordpress.org/reference/hooks/load-page_hook/
*/
public function loadPage(): void
protected function loadPage(): void
{
// By default do nothing.
}
Expand Down
28 changes: 18 additions & 10 deletions classes/BlueChip/Security/Core/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,16 @@ public function sanitize(array $settings, array $defaults = []): array
// Loop over default values instead of provided $settings - this way only known keys are preserved.
foreach ($values as $key => $default_value) {
if (isset($settings[$key])) {
// New value is provided, sanitize it either...
$values[$key] = isset(static::SANITIZERS[$key])
// ...using provided callback...
? \call_user_func(static::SANITIZERS[$key], $settings[$key], $default_value)
// ...or by type.
: self::sanitizeByType($settings[$key], $default_value)
;
// Sanitize the value by type first (= ensure the value has expected type).
$value = self::sanitizeByType($settings[$key], $default_value);

// If custom sanitizer for this setting key is provided...
if (isset(static::SANITIZERS[$key])) {
// ...execute it on type-safe value.
$value = \call_user_func(static::SANITIZERS[$key], $value, $default_value);
}

$values[$key] = $value;
}
}

Expand All @@ -249,8 +252,13 @@ public function sanitize(array $settings, array $defaults = []): array

/**
* Sanitize the $value according to type of $default value.
*
* @param mixed $value
* @param mixed[]|bool|float|int|string $default
*
* @return mixed[]|bool|float|int|string
*/
protected static function sanitizeByType(mixed $value, mixed $default): mixed
protected static function sanitizeByType(mixed $value, array|bool|float|int|string $default): array|bool|float|int|string
{
if (\is_bool($default)) {
return (bool) $value;
Expand Down Expand Up @@ -324,7 +332,7 @@ public function update(string $name, mixed $value): bool
*/
public function addUpdateHook(callable $callback): void
{
add_action("update_option_{$this->option_name}", [$this, 'updateOption'], 10, 2);
add_action("update_option_{$this->option_name}", $this->updateOption(...), 10, 2);
add_action("update_option_{$this->option_name}", $callback, 10, 3);
}

Expand All @@ -335,7 +343,7 @@ public function addUpdateHook(callable $callback): void
* @param array<string,mixed> $old_value
* @param array<string,mixed> $new_value
*/
public function updateOption(array $old_value, array $new_value): void
private function updateOption(array $old_value, array $new_value): void
{
$this->data = $new_value;
}
Expand Down
2 changes: 1 addition & 1 deletion classes/BlueChip/Security/Helpers/FormHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ abstract class FormHelper
* name and empty (false-like) value is printed before checkbox - this way,
* POST data contains value for checkbox even if it is left unchecked.
* Note that this approach works thanks to the fact that PHP retains value
* of the last key occurence in POST data when there are multiple occurences
* of the last key occurrence in POST data when there are multiple occurrences
* of the same key (name); when checkbox is checked (and included in POST),
* its value overwrites hidden field value.
* See: http://stackoverflow.com/a/1992745
Expand Down
14 changes: 8 additions & 6 deletions classes/BlueChip/Security/Modules/Access/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function load(): void
// I have to balance two requirements here:
// 1) Run the access check as early as possible (I consider `init` hook too late).
// 2) Allow myself and others to hook stuff (ie. events logger) in a clean way before access check executes.
add_action('plugins_loaded', [$this, 'checkAccess'], 1, 0);
add_action('plugins_loaded', $this->checkAccess(...), 1, 0);
}


Expand All @@ -50,7 +50,7 @@ public function load(): void
*/
public function init(): void
{
add_filter('authenticate', [$this, 'checkLoginAttempt'], 1, 1); // Leave priority 0 for site maintainers.
add_filter('authenticate', $this->checkLoginAttempt(...), 1, 1); // Leave priority 0 for site maintainers.
}


Expand Down Expand Up @@ -80,12 +80,12 @@ public function isBlocked(Scope $access_scope): bool
}


//// Hookers - public methods that should in fact be private

/**
* Check if access to website is allowed from given remote address.
*
* @action https://developer.wordpress.org/reference/hooks/plugins_loaded/
*/
public function checkAccess(): void
private function checkAccess(): void
{
if ($this->isBlocked(Scope::WEBSITE)) {
Utils::blockAccessTemporarily($this->remote_address);
Expand All @@ -95,8 +95,10 @@ public function checkAccess(): void

/**
* Check if access to login is allowed from given remote address.
*
* @filter https://developer.wordpress.org/reference/hooks/authenticate/
*/
public function checkLoginAttempt(WP_Error|WP_User|null $user): WP_Error|WP_User|null
private function checkLoginAttempt(WP_Error|WP_User|null $user): WP_Error|WP_User|null
{
if ($this->isBlocked(Scope::ADMIN)) {
Utils::blockAccessTemporarily($this->remote_address);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function __construct(Settings $settings, HtaccessSynchronizer $htaccess_s
}


public function loadPage(): void
protected function loadPage(): void
{
$this->displaySettingsErrors();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

abstract class BuiltInRules
{
public const ARCHIVE_FILES = 'archive-files';

private const ARCHIVE_FILES_PATTERN = '\.(tgz|zip)$';

public const BACKUP_FILES = 'backup-files';

private const BACKUP_FILES_PATTERN = 'backup|(\.(back|old|tmp)$)';
Expand Down Expand Up @@ -34,6 +38,11 @@ public static function enlist(): array
self::README_FILES_PATTERN,
__('(any URI targeting /readme.txt file)', 'bc-security')
),
self::ARCHIVE_FILES => new BanRule(
__('Non-existent archive files', 'bc-security'),
self::ARCHIVE_FILES_PATTERN,
__('(any URI targeting file with .tgz or .zip extension)', 'bc-security')
),
self::BACKUP_FILES => new BanRule(
__('Non-existent backup files', 'bc-security'),
self::BACKUP_FILES_PATTERN,
Expand Down
Loading

0 comments on commit 0122d94

Please sign in to comment.