initial
This commit is contained in:
@@ -0,0 +1,688 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.28.10] - 2026-06-25
|
||||
### Changed
|
||||
- Defer WAF REST controller registration so the controller class only loads on REST API requests. [#49803]
|
||||
|
||||
## [0.28.9] - 2026-06-22
|
||||
### Fixed
|
||||
- Retry key generation after transient activation failures. [#49768]
|
||||
|
||||
## [0.28.8] - 2026-06-15
|
||||
### Fixed
|
||||
- Guard against malformed allowlist data. [#49481]
|
||||
- Skip rule evaluation when there is no HTTP request (e.g. server-side cron executed via a PHP CLI wrapper), preventing false-positive 403 blocks. [#49465]
|
||||
|
||||
## [0.28.7] - 2026-06-08
|
||||
### Fixed
|
||||
- Brute Force Protection: Avoid a fatal error when rendering the login recovery form while an output buffer handler is active. [#49404]
|
||||
|
||||
## [0.28.6] - 2026-06-01
|
||||
### Fixed
|
||||
- Brute Force Protection: Prevent PHP warning when username is invalid. [#49292]
|
||||
|
||||
## [0.28.5] - 2026-05-25
|
||||
### Fixed
|
||||
- Phan: Address `PhanPluginDuplicateConditionalNullCoalescing` violations. [#48887]
|
||||
|
||||
## [0.28.4] - 2026-05-19
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.28.3] - 2026-05-04
|
||||
### Changed
|
||||
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
|
||||
|
||||
## [0.28.2] - 2026-03-23
|
||||
### Security
|
||||
- WAF: Fix issue that potentially allowed bypassing WAF rules. [#47692]
|
||||
|
||||
## [0.28.1] - 2026-02-23
|
||||
### Changed
|
||||
- Update dependencies. [#39263]
|
||||
|
||||
## [0.28.0] - 2026-02-16
|
||||
### Added
|
||||
- Enable support for ModSecurity's `MATCHED_VAR*` family of targets. [#47012]
|
||||
|
||||
## [0.27.11] - 2026-02-10
|
||||
### Fixed
|
||||
- Fix mishandling of empty Content-Type headers. [#46986]
|
||||
|
||||
## [0.27.10] - 2026-02-02
|
||||
### Changed
|
||||
- Update dependencies. [#39263]
|
||||
|
||||
## [0.27.9] - 2025-12-15
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.27.8] - 2025-12-08
|
||||
### Fixed
|
||||
- Ensure proper flags are used with `json_encode()`. [#46092]
|
||||
|
||||
## [0.27.7] - 2025-11-17
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.27.6] - 2025-11-03
|
||||
### Fixed
|
||||
- Phan: Address PhanRedundantCondition, PhanRedundantArrayValuesCall, and PhanPluginRedundantAssignment violations. [#45681]
|
||||
|
||||
## [0.27.5] - 2025-10-20
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.27.4] - 2025-09-15
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.27.3] - 2025-09-08
|
||||
### Fixed
|
||||
- WAF: Prevent PHP warnings when the BFP transient is not set or when the hook data for the WAF update flag is not as expected. [#45088]
|
||||
|
||||
## [0.27.2] - 2025-08-11
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.27.1] - 2025-07-21
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.27.0] - 2025-07-14
|
||||
### Changed
|
||||
- VIP: Change hosting check method. [#44223]
|
||||
|
||||
## [0.26.0] - 2025-07-03
|
||||
### Added
|
||||
- Add account recovery flow for blocklisted IP addresses. [#43051]
|
||||
|
||||
### Fixed
|
||||
- Fix PHP warnings for `Brute_Force_Protection->get_local_host()`. [#44170]
|
||||
|
||||
## [0.25.0] - 2025-06-23
|
||||
### Changed
|
||||
- Tests: Modify environment check in tests to match new requirements. [#44059]
|
||||
|
||||
## [0.24.4] - 2025-06-09
|
||||
### Fixed
|
||||
- Tests: Ensure method param count matches PHPUnit data providers. [#43815] [#43828]
|
||||
|
||||
## [0.24.3] - 2025-04-28
|
||||
### Fixed
|
||||
- Linting: Fix more Stylelint violations. [#43213]
|
||||
|
||||
## [0.24.2] - 2025-04-21
|
||||
### Fixed
|
||||
- Brute Force Protection: Handle unexpected parameter types from third-party plugins during login failure processing. [#43119]
|
||||
|
||||
## [0.24.1] - 2025-04-01
|
||||
### Changed
|
||||
- Add a default value for the error param in the `wp_login_failed` action callback. [#42819]
|
||||
|
||||
## [0.24.0] - 2025-03-31
|
||||
### Changed
|
||||
- Ensures Brute Force Protection does not log failed attempts on password validation failure. [#40925]
|
||||
|
||||
## [0.23.8] - 2025-03-24
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.23.7] - 2025-03-17
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.23.6] - 2025-03-12
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.23.5] - 2025-03-10
|
||||
### Changed
|
||||
- Ensure check_valid_blocked_user handles error outcomes. [#42036]
|
||||
|
||||
## [0.23.4] - 2025-02-24
|
||||
### Changed
|
||||
- Update dependencies. [#39263]
|
||||
|
||||
## [0.23.3] - 2025-02-03
|
||||
### Fixed
|
||||
- Code: Remove extra params on function calls. [#41263]
|
||||
|
||||
## [0.23.2] - 2025-01-20
|
||||
### Changed
|
||||
- Code: Use function-style exit() and die() with a default status code of 0. [#41167]
|
||||
|
||||
## [0.23.1] - 2024-11-25
|
||||
### Changed
|
||||
- Updated dependencies. [#40286]
|
||||
|
||||
## [0.23.0] - 2024-11-18
|
||||
### Removed
|
||||
- General: Update minimum PHP version to 7.2. [#40147]
|
||||
|
||||
## [0.22.3] - 2024-11-04
|
||||
### Added
|
||||
- Enable test coverage. [#39961]
|
||||
|
||||
## [0.22.2] - 2024-10-29
|
||||
### Changed
|
||||
- Internal updates. [#39263]
|
||||
|
||||
## [0.22.1] - 2024-10-17
|
||||
### Fixed
|
||||
- WAF: Improve backwards compatibility for sites running outdated bootstrap scripts via standalone mode. [#39812]
|
||||
|
||||
## [0.22.0] - 2024-10-14
|
||||
### Added
|
||||
- WAF: Add new properties to the WAF feature's REST API endpoint. [#39511]
|
||||
|
||||
### Fixed
|
||||
- Improve backwards compatibility for sites running in standalone mode. [#39652]
|
||||
- WAF: Reduce amount of classes autoloaded during standalone mode execution. [#38944]
|
||||
|
||||
## [0.21.0] - 2024-10-07
|
||||
### Added
|
||||
- Firewall Runtime: Added support for rule files to specify body parser type. [#39516]
|
||||
|
||||
## [0.20.1] - 2024-10-01
|
||||
### Deprecated
|
||||
- Added back public API as deprecated. [#39606]
|
||||
|
||||
## [0.20.0] - 2024-09-30
|
||||
### Added
|
||||
- Added Waf_Blocklog_Manager class [#35739]
|
||||
|
||||
## [0.19.0] - 2024-09-23
|
||||
### Added
|
||||
- Firewall: add support for CIDR ranges in IP lists. [#39425]
|
||||
|
||||
## [0.18.5] - 2024-09-06
|
||||
### Changed
|
||||
- Updated package dependencies. [#39253]
|
||||
|
||||
### Fixed
|
||||
- Ensure that request body is parsed correctly [#39262]
|
||||
|
||||
## [0.18.4] - 2024-08-26
|
||||
### Changed
|
||||
- Updated package dependencies. [#39004]
|
||||
|
||||
## [0.18.3] - 2024-08-19
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.18.2] - 2024-08-15
|
||||
### Fixed
|
||||
- Fix incorrect next-version tokens in php `@since` and/or `@deprecated` docs. [#38869]
|
||||
|
||||
## [0.18.1] - 2024-08-08
|
||||
### Security
|
||||
- Parse request body when method used is not POST [#38621]
|
||||
|
||||
### Added
|
||||
- Brute Force Protection: Add `jetpack_has_login_ability` hook. [#38518]
|
||||
|
||||
## [0.18.0] - 2024-08-01
|
||||
### Added
|
||||
- Adds global statistics [#38388]
|
||||
|
||||
### Fixed
|
||||
- Fix global stats type check [#38634]
|
||||
|
||||
## [0.17.0] - 2024-07-22
|
||||
### Added
|
||||
- Added the ability to toggle IP block and allow lists individually. [#38184]
|
||||
|
||||
## [0.16.10] - 2024-06-26
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.9] - 2024-06-03
|
||||
### Changed
|
||||
- Phab baseline file update. [#36968]
|
||||
|
||||
## [0.16.8] - 2024-05-20
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.7] - 2024-05-06
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.6] - 2024-04-29
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.5] - 2024-04-25
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.4] - 2024-04-22
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.3] - 2024-04-15
|
||||
### Security
|
||||
- Improves handling of REQUEST_URI. [#36833]
|
||||
|
||||
## [0.16.2] - 2024-04-08
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.1] - 2024-03-25
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.16.0] - 2024-03-22
|
||||
### Added
|
||||
- Add data to WAF logs and add toggle for users to opt-in to share more data with us if needed. [#36377]
|
||||
|
||||
## [0.15.2] - 2024-03-18
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.15.1] - 2024-03-14
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.15.0] - 2024-03-12
|
||||
### Added
|
||||
- Add JSON parameter support to the Web Application Firewall. [#36169]
|
||||
|
||||
## [0.14.2] - 2024-03-04
|
||||
### Fixed
|
||||
- Fixed base64 transforms to better conform with the modsecurity runtime [#35693]
|
||||
|
||||
## [0.14.1] - 2024-02-27
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.14.0] - 2024-02-12
|
||||
### Added
|
||||
- Add standalone mode status to WAF config [#34840]
|
||||
|
||||
## [0.13.0] - 2024-02-05
|
||||
### Added
|
||||
- Run the WAF on JN environments [#35341]
|
||||
|
||||
## [0.12.4] - 2024-01-18
|
||||
### Fixed
|
||||
- Optimize how the web application firewall checks for updates on admin screens. [#34820]
|
||||
|
||||
## [0.12.3] - 2024-01-02
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.12.2] - 2023-12-25
|
||||
### Changed
|
||||
- Improve top-level WP-CLI command description [#34745]
|
||||
|
||||
## [0.12.1] - 2023-11-21
|
||||
|
||||
## [0.12.0] - 2023-11-20
|
||||
### Changed
|
||||
- Updated required PHP version to >= 7.0. [#34192]
|
||||
|
||||
## [0.11.15] - 2023-11-14
|
||||
|
||||
## [0.11.14] - 2023-10-30
|
||||
|
||||
## [0.11.13] - 2023-10-10
|
||||
### Fixed
|
||||
- Escape email address when output in HTML. [#33536]
|
||||
|
||||
## [0.11.12] - 2023-09-28
|
||||
### Changed
|
||||
- Minor internal updates.
|
||||
|
||||
## [0.11.11] - 2023-09-19
|
||||
|
||||
- Minor internal updates.
|
||||
|
||||
## [0.11.10] - 2023-09-11
|
||||
### Changed
|
||||
- General: remove backwards-compatible functions now that package relies on WordPress 6.2. [#32772]
|
||||
|
||||
## [0.11.9] - 2023-08-28
|
||||
### Changed
|
||||
- Updated package dependencies. [#32605]
|
||||
|
||||
## [0.11.8] - 2023-07-18
|
||||
### Changed
|
||||
- Add support for running brute force protection in environments that otherwise do not support the WAF. [#31761]
|
||||
- Minor performance improvements. [#31684]
|
||||
|
||||
## [0.11.7] - 2023-07-17
|
||||
### Changed
|
||||
- Add support for non-empty server https values. [#31688]
|
||||
|
||||
## [0.11.6] - 2023-05-22
|
||||
### Added
|
||||
- Add integration tests for unsupported environments [#30544]
|
||||
|
||||
### Fixed
|
||||
- Fix Brute force protection activation when WAF unset [#30544]
|
||||
- Fix unavailable endpoint when WAF module is disabled [#30487]
|
||||
- Multisite: avoid errors when the package is used in the Protect plugin instead of the Jetpack plugin. [#30767]
|
||||
|
||||
## [0.11.5] - 2023-05-15
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.11.4] - 2023-04-27
|
||||
### Added
|
||||
- Fix hardblock issue if user only has Protect installed [#30278]
|
||||
|
||||
## [0.11.3] - 2023-04-17
|
||||
### Fixed
|
||||
- Fix brute force protection not initializing on atomic. [#30113]
|
||||
|
||||
## [0.11.2] - 2023-04-10
|
||||
### Added
|
||||
- Add Jetpack Autoloader package suggestion. [#29988]
|
||||
|
||||
## [0.11.1] - 2023-04-03
|
||||
### Fixed
|
||||
- Return early if we detect the older BFP implementation from the main plugin [#29794]
|
||||
|
||||
## [0.11.0] - 2023-03-28
|
||||
### Added
|
||||
- Added brute force protection to the WAF configuration REST API endpoints [#28401]
|
||||
- Move the brute force protection module into the package. [#28401]
|
||||
|
||||
### Changed
|
||||
- Change "whitelist" to "allow list". [#28401]
|
||||
- Move the brute force protection transient cleanup and shared functions to dedicated namespaced classes. [#28401]
|
||||
- Use WAF IP allow list option in brute force protection feature. [#28401]
|
||||
|
||||
## [0.10.2] - 2023-03-20
|
||||
### Changed
|
||||
- Updated package dependencies. [#29480]
|
||||
|
||||
## [0.10.1] - 2023-03-08
|
||||
### Changed
|
||||
- Minor internal updates.
|
||||
|
||||
## [0.10.0] - 2023-02-28
|
||||
### Added
|
||||
- Added support for IP ranges in allow and block lists. [#29131]
|
||||
|
||||
## [0.9.3] - 2023-02-20
|
||||
### Changed
|
||||
- Minor internal updates.
|
||||
|
||||
## [0.9.2] - 2023-02-15
|
||||
### Changed
|
||||
- Minor internal updates.
|
||||
|
||||
## [0.9.1] - 2023-02-13
|
||||
### Fixed
|
||||
- Fix an update error that impacted sites using the WAF in standalone mode. [#28844]
|
||||
|
||||
## [0.9.0] - 2023-01-25
|
||||
### Changed
|
||||
- Change the web application firewall to run automatic and manual rules independently. [#27726]
|
||||
|
||||
## [0.8.3] - 2023-01-11
|
||||
### Fixed
|
||||
- Fixed the WAF package's PHP tests and Composer requirements [#28185]
|
||||
|
||||
## [0.8.2] - 2023-01-09
|
||||
### Fixed
|
||||
- Fix firewall activation hooks on first option updates. [#28234]
|
||||
|
||||
## [0.8.1] - 2023-01-07
|
||||
### Changed
|
||||
- Change directory location that stores firewall rules. [#28049]
|
||||
|
||||
## [0.8.0] - 2022-12-27
|
||||
### Added
|
||||
- Add file existance checks before requiring rule files in the WAF. [#28050]
|
||||
- Disable Jetpack Firewall on unsupported environments. [#27939]
|
||||
|
||||
## [0.7.2] - 2022-12-19
|
||||
### Fixed
|
||||
- Fix the initialization of the firewall. [#27846]
|
||||
|
||||
## [0.7.1] - 2022-12-06
|
||||
### Changed
|
||||
- html_entity_decode filter now decodes single-quotes too, and uses a Unicode Replacement Character instead of returning empty string on invalid characters. [#27753]
|
||||
|
||||
## [0.7.0] - 2022-12-05
|
||||
### Added
|
||||
- Prepare package for use in the Jetpack Protect standalone plugin. [#27528]
|
||||
|
||||
### Changed
|
||||
- Updated package dependencies. [#27688]
|
||||
|
||||
### Removed
|
||||
- Remove has_rules_access plan check in favor of external alternatives [#27600]
|
||||
|
||||
## [0.6.10] - 2022-11-28
|
||||
### Changed
|
||||
- Updated package dependencies. [#27043]
|
||||
|
||||
## [0.6.9] - 2022-11-01
|
||||
### Fixed
|
||||
- Fix bug for cron event not generating IP rules. [#27215]
|
||||
|
||||
## [0.6.8] - 2022-10-27
|
||||
### Fixed
|
||||
- Fixes several invalid action callbacks. [#27106]
|
||||
|
||||
## [0.6.7] - 2022-09-20
|
||||
### Changed
|
||||
- Changing how we load and run the package to avoid actions.php [#24730]
|
||||
|
||||
## [0.6.6] - 2022-09-08
|
||||
### Fixed
|
||||
- Fixed exception namespace. [#25663]
|
||||
|
||||
## [0.6.5] - 2022-07-26
|
||||
### Changed
|
||||
- Updated package dependencies. [#25158]
|
||||
|
||||
## [0.6.4] - 2022-07-12
|
||||
### Fixed
|
||||
- Correct namespacing error. [#24993]
|
||||
|
||||
## [0.6.3] - 2022-06-21
|
||||
### Changed
|
||||
- Renaming master to trunk. [#24661]
|
||||
|
||||
## [0.6.2] - 2022-06-06
|
||||
### Fixed
|
||||
- Fix the hook we're using for run.php.
|
||||
|
||||
## [0.6.1] - 2022-06-02
|
||||
### Removed
|
||||
- Disable the WAF module on Atomic
|
||||
|
||||
## [0.6.0] - 2022-05-18
|
||||
### Added
|
||||
- Add checks for a killswitch define [#24247]
|
||||
- Added endpoint to update rules on demand [#24327]
|
||||
- handle share data option to decide if we should write to log file [#24218]
|
||||
|
||||
### Fixed
|
||||
- Allow the rules API to return 401 responses without throwing an exception. [#24153]
|
||||
- fix bootstrap generation in cases file.php is not required yet [#24153]
|
||||
|
||||
## [0.5.1] - 2022-05-04
|
||||
### Added
|
||||
- Added a check to only run the firewall when the Jetpack module is enabled, a method to provide the bootstrap.php path, and a REST API endpoint to provide the firewall settings. [#23769]
|
||||
- Connected the WAF UI to actually updating the IP block and allow lists when saving the settings. [#24124]
|
||||
|
||||
### Fixed
|
||||
- Fixed database logging [#24070]
|
||||
- Fixed issue where code for the waf package was executed if the module was disabled [#24217]
|
||||
- Fixed writing rules php files if the API request for getting up-to-date rules failes so that the internal functionality is kept in tact. [#24181]
|
||||
- We now sanitize the output generated by blocked requests, and only report the rule ID in the header response. [#24058]
|
||||
|
||||
## [0.5.0] - 2022-04-26
|
||||
### Added
|
||||
- added cron to update rules
|
||||
- Added WAF IP allow list and block list functionality.
|
||||
|
||||
### Changed
|
||||
- Added comment to ignore failing phpcs check
|
||||
- PHPCS: Fix `WordPress.Security.ValidatedSanitizedInput`
|
||||
- Updated package dependencies.
|
||||
|
||||
## [0.4.0] - 2022-04-19
|
||||
### Added
|
||||
- added logs when a request is blocked
|
||||
- Generating rules now fetches them from the API. Also adds a few CLI commands.
|
||||
|
||||
## [0.3.0] - 2022-04-12
|
||||
### Added
|
||||
- Added hooks for generating the rules.php file, and improved functionality and class names.
|
||||
|
||||
## [0.2.0] - 2022-04-06
|
||||
### Added
|
||||
- Added Jetpack WAF standalone mode.
|
||||
|
||||
### Fixed
|
||||
- Fix normalizing nested array targets, like with query strings.
|
||||
|
||||
## [0.1.1] - 2022-03-29
|
||||
### Fixed
|
||||
- Fixed instance of normalizeHeaderName that wasn't renamed; fixed header parsing; removed unused compiler file.
|
||||
|
||||
## 0.1.0 - 2022-02-16
|
||||
### Added
|
||||
- Added executing the WAF as part of the Jetpack plugin.
|
||||
- Added Initial version
|
||||
|
||||
### Changed
|
||||
- Core: do not ship .phpcs.dir.xml in production builds.
|
||||
|
||||
[0.28.10]: https://github.com/Automattic/jetpack-waf/compare/v0.28.9...v0.28.10
|
||||
[0.28.9]: https://github.com/Automattic/jetpack-waf/compare/v0.28.8...v0.28.9
|
||||
[0.28.8]: https://github.com/Automattic/jetpack-waf/compare/v0.28.7...v0.28.8
|
||||
[0.28.7]: https://github.com/Automattic/jetpack-waf/compare/v0.28.6...v0.28.7
|
||||
[0.28.6]: https://github.com/Automattic/jetpack-waf/compare/v0.28.5...v0.28.6
|
||||
[0.28.5]: https://github.com/Automattic/jetpack-waf/compare/v0.28.4...v0.28.5
|
||||
[0.28.4]: https://github.com/Automattic/jetpack-waf/compare/v0.28.3...v0.28.4
|
||||
[0.28.3]: https://github.com/Automattic/jetpack-waf/compare/v0.28.2...v0.28.3
|
||||
[0.28.2]: https://github.com/Automattic/jetpack-waf/compare/v0.28.1...v0.28.2
|
||||
[0.28.1]: https://github.com/Automattic/jetpack-waf/compare/v0.28.0...v0.28.1
|
||||
[0.28.0]: https://github.com/Automattic/jetpack-waf/compare/v0.27.11...v0.28.0
|
||||
[0.27.11]: https://github.com/Automattic/jetpack-waf/compare/v0.27.10...v0.27.11
|
||||
[0.27.10]: https://github.com/Automattic/jetpack-waf/compare/v0.27.9...v0.27.10
|
||||
[0.27.9]: https://github.com/Automattic/jetpack-waf/compare/v0.27.8...v0.27.9
|
||||
[0.27.8]: https://github.com/Automattic/jetpack-waf/compare/v0.27.7...v0.27.8
|
||||
[0.27.7]: https://github.com/Automattic/jetpack-waf/compare/v0.27.6...v0.27.7
|
||||
[0.27.6]: https://github.com/Automattic/jetpack-waf/compare/v0.27.5...v0.27.6
|
||||
[0.27.5]: https://github.com/Automattic/jetpack-waf/compare/v0.27.4...v0.27.5
|
||||
[0.27.4]: https://github.com/Automattic/jetpack-waf/compare/v0.27.3...v0.27.4
|
||||
[0.27.3]: https://github.com/Automattic/jetpack-waf/compare/v0.27.2...v0.27.3
|
||||
[0.27.2]: https://github.com/Automattic/jetpack-waf/compare/v0.27.1...v0.27.2
|
||||
[0.27.1]: https://github.com/Automattic/jetpack-waf/compare/v0.27.0...v0.27.1
|
||||
[0.27.0]: https://github.com/Automattic/jetpack-waf/compare/v0.26.0...v0.27.0
|
||||
[0.26.0]: https://github.com/Automattic/jetpack-waf/compare/v0.25.0...v0.26.0
|
||||
[0.25.0]: https://github.com/Automattic/jetpack-waf/compare/v0.24.4...v0.25.0
|
||||
[0.24.4]: https://github.com/Automattic/jetpack-waf/compare/v0.24.3...v0.24.4
|
||||
[0.24.3]: https://github.com/Automattic/jetpack-waf/compare/v0.24.2...v0.24.3
|
||||
[0.24.2]: https://github.com/Automattic/jetpack-waf/compare/v0.24.1...v0.24.2
|
||||
[0.24.1]: https://github.com/Automattic/jetpack-waf/compare/v0.24.0...v0.24.1
|
||||
[0.24.0]: https://github.com/Automattic/jetpack-waf/compare/v0.23.8...v0.24.0
|
||||
[0.23.8]: https://github.com/Automattic/jetpack-waf/compare/v0.23.7...v0.23.8
|
||||
[0.23.7]: https://github.com/Automattic/jetpack-waf/compare/v0.23.6...v0.23.7
|
||||
[0.23.6]: https://github.com/Automattic/jetpack-waf/compare/v0.23.5...v0.23.6
|
||||
[0.23.5]: https://github.com/Automattic/jetpack-waf/compare/v0.23.4...v0.23.5
|
||||
[0.23.4]: https://github.com/Automattic/jetpack-waf/compare/v0.23.3...v0.23.4
|
||||
[0.23.3]: https://github.com/Automattic/jetpack-waf/compare/v0.23.2...v0.23.3
|
||||
[0.23.2]: https://github.com/Automattic/jetpack-waf/compare/v0.23.1...v0.23.2
|
||||
[0.23.1]: https://github.com/Automattic/jetpack-waf/compare/v0.23.0...v0.23.1
|
||||
[0.23.0]: https://github.com/Automattic/jetpack-waf/compare/v0.22.3...v0.23.0
|
||||
[0.22.3]: https://github.com/Automattic/jetpack-waf/compare/v0.22.2...v0.22.3
|
||||
[0.22.2]: https://github.com/Automattic/jetpack-waf/compare/v0.22.1...v0.22.2
|
||||
[0.22.1]: https://github.com/Automattic/jetpack-waf/compare/v0.22.0...v0.22.1
|
||||
[0.22.0]: https://github.com/Automattic/jetpack-waf/compare/v0.21.0...v0.22.0
|
||||
[0.21.0]: https://github.com/Automattic/jetpack-waf/compare/v0.20.1...v0.21.0
|
||||
[0.20.1]: https://github.com/Automattic/jetpack-waf/compare/v0.20.0...v0.20.1
|
||||
[0.20.0]: https://github.com/Automattic/jetpack-waf/compare/v0.19.0...v0.20.0
|
||||
[0.19.0]: https://github.com/Automattic/jetpack-waf/compare/v0.18.5...v0.19.0
|
||||
[0.18.5]: https://github.com/Automattic/jetpack-waf/compare/v0.18.4...v0.18.5
|
||||
[0.18.4]: https://github.com/Automattic/jetpack-waf/compare/v0.18.3...v0.18.4
|
||||
[0.18.3]: https://github.com/Automattic/jetpack-waf/compare/v0.18.2...v0.18.3
|
||||
[0.18.2]: https://github.com/Automattic/jetpack-waf/compare/v0.18.1...v0.18.2
|
||||
[0.18.1]: https://github.com/Automattic/jetpack-waf/compare/v0.18.0...v0.18.1
|
||||
[0.18.0]: https://github.com/Automattic/jetpack-waf/compare/v0.17.0...v0.18.0
|
||||
[0.17.0]: https://github.com/Automattic/jetpack-waf/compare/v0.16.10...v0.17.0
|
||||
[0.16.10]: https://github.com/Automattic/jetpack-waf/compare/v0.16.9...v0.16.10
|
||||
[0.16.9]: https://github.com/Automattic/jetpack-waf/compare/v0.16.8...v0.16.9
|
||||
[0.16.8]: https://github.com/Automattic/jetpack-waf/compare/v0.16.7...v0.16.8
|
||||
[0.16.7]: https://github.com/Automattic/jetpack-waf/compare/v0.16.6...v0.16.7
|
||||
[0.16.6]: https://github.com/Automattic/jetpack-waf/compare/v0.16.5...v0.16.6
|
||||
[0.16.5]: https://github.com/Automattic/jetpack-waf/compare/v0.16.4...v0.16.5
|
||||
[0.16.4]: https://github.com/Automattic/jetpack-waf/compare/v0.16.3...v0.16.4
|
||||
[0.16.3]: https://github.com/Automattic/jetpack-waf/compare/v0.16.2...v0.16.3
|
||||
[0.16.2]: https://github.com/Automattic/jetpack-waf/compare/v0.16.1...v0.16.2
|
||||
[0.16.1]: https://github.com/Automattic/jetpack-waf/compare/v0.16.0...v0.16.1
|
||||
[0.16.0]: https://github.com/Automattic/jetpack-waf/compare/v0.15.1...v0.16.0
|
||||
[0.15.2]: https://github.com/Automattic/jetpack-waf/compare/v0.15.1...v0.15.2
|
||||
[0.15.1]: https://github.com/Automattic/jetpack-waf/compare/v0.15.0...v0.15.1
|
||||
[0.15.0]: https://github.com/Automattic/jetpack-waf/compare/v0.14.2...v0.15.0
|
||||
[0.14.2]: https://github.com/Automattic/jetpack-waf/compare/v0.14.1...v0.14.2
|
||||
[0.14.1]: https://github.com/Automattic/jetpack-waf/compare/v0.14.0...v0.14.1
|
||||
[0.14.0]: https://github.com/Automattic/jetpack-waf/compare/v0.13.0...v0.14.0
|
||||
[0.13.0]: https://github.com/Automattic/jetpack-waf/compare/v0.12.4...v0.13.0
|
||||
[0.12.4]: https://github.com/Automattic/jetpack-waf/compare/v0.12.3...v0.12.4
|
||||
[0.12.3]: https://github.com/Automattic/jetpack-waf/compare/v0.12.2...v0.12.3
|
||||
[0.12.2]: https://github.com/Automattic/jetpack-waf/compare/v0.12.1...v0.12.2
|
||||
[0.12.1]: https://github.com/Automattic/jetpack-waf/compare/v0.12.0...v0.12.1
|
||||
[0.12.0]: https://github.com/Automattic/jetpack-waf/compare/v0.11.15...v0.12.0
|
||||
[0.11.15]: https://github.com/Automattic/jetpack-waf/compare/v0.11.14...v0.11.15
|
||||
[0.11.14]: https://github.com/Automattic/jetpack-waf/compare/v0.11.13...v0.11.14
|
||||
[0.11.13]: https://github.com/Automattic/jetpack-waf/compare/v0.11.12...v0.11.13
|
||||
[0.11.12]: https://github.com/Automattic/jetpack-waf/compare/v0.11.11...v0.11.12
|
||||
[0.11.11]: https://github.com/Automattic/jetpack-waf/compare/v0.11.10...v0.11.11
|
||||
[0.11.10]: https://github.com/Automattic/jetpack-waf/compare/v0.11.9...v0.11.10
|
||||
[0.11.9]: https://github.com/Automattic/jetpack-waf/compare/v0.11.8...v0.11.9
|
||||
[0.11.8]: https://github.com/Automattic/jetpack-waf/compare/v0.11.7...v0.11.8
|
||||
[0.11.7]: https://github.com/Automattic/jetpack-waf/compare/v0.11.6...v0.11.7
|
||||
[0.11.6]: https://github.com/Automattic/jetpack-waf/compare/v0.11.5...v0.11.6
|
||||
[0.11.5]: https://github.com/Automattic/jetpack-waf/compare/v0.11.4...v0.11.5
|
||||
[0.11.4]: https://github.com/Automattic/jetpack-waf/compare/v0.11.3...v0.11.4
|
||||
[0.11.3]: https://github.com/Automattic/jetpack-waf/compare/v0.11.2...v0.11.3
|
||||
[0.11.2]: https://github.com/Automattic/jetpack-waf/compare/v0.11.1...v0.11.2
|
||||
[0.11.1]: https://github.com/Automattic/jetpack-waf/compare/v0.11.0...v0.11.1
|
||||
[0.11.0]: https://github.com/Automattic/jetpack-waf/compare/v0.10.2...v0.11.0
|
||||
[0.10.2]: https://github.com/Automattic/jetpack-waf/compare/v0.10.1...v0.10.2
|
||||
[0.10.1]: https://github.com/Automattic/jetpack-waf/compare/v0.10.0...v0.10.1
|
||||
[0.10.0]: https://github.com/Automattic/jetpack-waf/compare/v0.9.3...v0.10.0
|
||||
[0.9.3]: https://github.com/Automattic/jetpack-waf/compare/v0.9.2...v0.9.3
|
||||
[0.9.2]: https://github.com/Automattic/jetpack-waf/compare/v0.9.1...v0.9.2
|
||||
[0.9.1]: https://github.com/Automattic/jetpack-waf/compare/v0.9.0...v0.9.1
|
||||
[0.9.0]: https://github.com/Automattic/jetpack-waf/compare/v0.8.3...v0.9.0
|
||||
[0.8.3]: https://github.com/Automattic/jetpack-waf/compare/v0.8.2...v0.8.3
|
||||
[0.8.2]: https://github.com/Automattic/jetpack-waf/compare/v0.8.1...v0.8.2
|
||||
[0.8.1]: https://github.com/Automattic/jetpack-waf/compare/v0.8.0...v0.8.1
|
||||
[0.8.0]: https://github.com/Automattic/jetpack-waf/compare/v0.7.2...v0.8.0
|
||||
[0.7.2]: https://github.com/Automattic/jetpack-waf/compare/v0.7.1...v0.7.2
|
||||
[0.7.1]: https://github.com/Automattic/jetpack-waf/compare/v0.7.0...v0.7.1
|
||||
[0.7.0]: https://github.com/Automattic/jetpack-waf/compare/v0.6.10...v0.7.0
|
||||
[0.6.10]: https://github.com/Automattic/jetpack-waf/compare/v0.6.9...v0.6.10
|
||||
[0.6.9]: https://github.com/Automattic/jetpack-waf/compare/v0.6.8...v0.6.9
|
||||
[0.6.8]: https://github.com/Automattic/jetpack-waf/compare/v0.6.7...v0.6.8
|
||||
[0.6.7]: https://github.com/Automattic/jetpack-waf/compare/v0.6.6...v0.6.7
|
||||
[0.6.6]: https://github.com/Automattic/jetpack-waf/compare/v0.6.5...v0.6.6
|
||||
[0.6.5]: https://github.com/Automattic/jetpack-waf/compare/v0.6.4...v0.6.5
|
||||
[0.6.4]: https://github.com/Automattic/jetpack-waf/compare/v0.6.3...v0.6.4
|
||||
[0.6.3]: https://github.com/Automattic/jetpack-waf/compare/v0.6.2...v0.6.3
|
||||
[0.6.2]: https://github.com/Automattic/jetpack-waf/compare/v0.6.1...v0.6.2
|
||||
[0.6.1]: https://github.com/Automattic/jetpack-waf/compare/v0.6.0...v0.6.1
|
||||
[0.6.0]: https://github.com/Automattic/jetpack-waf/compare/v0.5.1...v0.6.0
|
||||
[0.5.1]: https://github.com/Automattic/jetpack-waf/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/Automattic/jetpack-waf/compare/v0.4.0...v0.5.0
|
||||
[0.4.0]: https://github.com/Automattic/jetpack-waf/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/Automattic/jetpack-waf/compare/v0.2.0...v0.3.0
|
||||
[0.2.0]: https://github.com/Automattic/jetpack-waf/compare/v0.1.1...v0.2.0
|
||||
[0.1.1]: https://github.com/Automattic/jetpack-waf/compare/v0.1.0...v0.1.1
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* Registers the CLI functionality.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
if ( defined( 'WP_CLI' ) && \WP_CLI ) {
|
||||
\WP_CLI::add_command( 'jetpack-waf', CLI::class );
|
||||
}
|
||||
+779
@@ -0,0 +1,779 @@
|
||||
<?php // phpcs:ignore - WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Class Blocked_Login_Page
|
||||
*
|
||||
* Instantiated on the wp-login page when Jetpack modules are loaded and $pagenow
|
||||
* is available, or during the login_head hook.
|
||||
*
|
||||
* Class will only be instantiated if a hard blocked IP address is detected.
|
||||
*/
|
||||
abstract class Blocked_Login_Page {
|
||||
|
||||
/**
|
||||
* Instance of the class.
|
||||
*
|
||||
* @var Blocked_Login_Page
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Can send recovery emails. defaults to true.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $can_send_recovery_emails;
|
||||
|
||||
/**
|
||||
* The IP address.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $ip_address;
|
||||
|
||||
/**
|
||||
* Valid blocked user ID.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $valid_blocked_user_id;
|
||||
|
||||
/**
|
||||
* The email address.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $email_address;
|
||||
|
||||
/**
|
||||
* Status code for too many requests.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429;
|
||||
|
||||
/**
|
||||
* Singleton implementation
|
||||
*
|
||||
* @param string $ip_address - the IP address.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public static function instance( $ip_address ) {
|
||||
if ( ! self::$instance ) {
|
||||
// @phan-suppress-next-line PhanTypeInstantiateAbstractStatic -- This is only instantiated in non-abstract classes (i.e. Waf_Blocked_Login_Page).
|
||||
self::$instance = new static( $ip_address );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton implementation
|
||||
*
|
||||
* @param string $ip_address - the IP address.
|
||||
*/
|
||||
public function __construct( $ip_address ) {
|
||||
/**
|
||||
* Filter controls if an email recovery form is shown to blocked IPs.
|
||||
*
|
||||
* A recovery form allows folks to re-gain access to the login form
|
||||
* via an email link if their IP was mistakenly blocked.
|
||||
*
|
||||
* @module protect
|
||||
*
|
||||
* @since 5.6.0
|
||||
*
|
||||
* @param bool $can_send_recovery_emails Defaults to true.
|
||||
*/
|
||||
$this->can_send_recovery_emails = apply_filters( 'jetpack_protect_can_send_recovery_emails', true );
|
||||
$this->ip_address = $ip_address;
|
||||
|
||||
add_filter( 'wp_authenticate_user', array( $this, 'check_valid_blocked_user' ), 10, 1 );
|
||||
add_filter( 'site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
|
||||
add_filter( 'network_site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
|
||||
add_filter( 'lostpassword_url', array( $this, 'add_args_to_lostpassword_url' ), 10, 2 );
|
||||
add_filter( 'login_url', array( $this, 'add_args_to_login_url' ), 10, 3 );
|
||||
add_filter( 'lostpassword_redirect', array( $this, 'add_args_to_lostpassword_redirect_url' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL that redirects to the support page on unblocking
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_help_url();
|
||||
|
||||
/**
|
||||
* Add arguments to lost password redirect url.
|
||||
*
|
||||
* @param string $url - the URL.
|
||||
*/
|
||||
public function add_args_to_lostpassword_redirect_url( $url ) {
|
||||
if ( $this->valid_blocked_user_id ) {
|
||||
$url = empty( $url ) ? wp_login_url() : $url;
|
||||
$url = add_query_arg(
|
||||
array(
|
||||
'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
'user_id' => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
'checkemail' => 'confirm',
|
||||
),
|
||||
$url
|
||||
);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add arguments to lost password redirect url.
|
||||
*
|
||||
* @param string $url - the URL.
|
||||
* @param string $redirect - where to redirect to.
|
||||
*/
|
||||
public function add_args_to_lostpassword_url( $url, $redirect ) {
|
||||
if ( $this->valid_blocked_user_id ) {
|
||||
$args = array(
|
||||
'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
'user_id' => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
'action' => 'lostpassword',
|
||||
);
|
||||
if ( ! empty( $redirect ) ) {
|
||||
$args['redirect_to'] = $redirect;
|
||||
}
|
||||
$url = add_query_arg( $args, $url );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add arguments to login post url.
|
||||
*
|
||||
* @param string $url - the URL.
|
||||
* @param string $path - the path.
|
||||
* @param string $scheme -the scheme(?).
|
||||
*/
|
||||
public function add_args_to_login_post_url( $url, $path, $scheme ) {
|
||||
if ( $this->valid_blocked_user_id && ( 'login_post' === $scheme || 'login' === $scheme ) ) {
|
||||
$url = add_query_arg(
|
||||
array(
|
||||
'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
'user_id' => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
),
|
||||
$url
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add arguments to login post url.
|
||||
*
|
||||
* @param string $url - the URL.
|
||||
* @param string $redirect - where we want to redirect to.
|
||||
* @param string $force_reauth -if we're forcing reauthorization.
|
||||
*/
|
||||
public function add_args_to_login_url( $url, $redirect, $force_reauth ) {
|
||||
if ( $this->valid_blocked_user_id ) {
|
||||
$args = array(
|
||||
'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
'user_id' => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
);
|
||||
|
||||
if ( ! empty( $redirect ) ) {
|
||||
$args['redirect_to'] = $redirect;
|
||||
}
|
||||
|
||||
if ( ! empty( $force_reauth ) ) {
|
||||
$args['reauth'] = '1';
|
||||
}
|
||||
$url = add_query_arg( $args, $url );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is blocked.
|
||||
*
|
||||
* @param WP_User|WP_Error $user - The user or error object if prior callback failed auth.
|
||||
*/
|
||||
public function check_valid_blocked_user( $user ) {
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
if ( $this->valid_blocked_user_id && $this->valid_blocked_user_id != $user->ID ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
|
||||
return new WP_Error( 'invalid_recovery_token', __( 'The recovery token is not valid for this user.', 'jetpack-waf' ) );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is valid.
|
||||
*/
|
||||
public function is_blocked_user_valid() {
|
||||
if ( ! $this->can_send_recovery_emails ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->valid_blocked_user_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! isset( $_GET['validate_jetpack_protect_recovery'] ) || ! isset( $_GET['user_id'] ) ) { // phpcs:ignore: WordPress.Security.NonceVerification.Recommended -- no changes made if this isn't set.
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->is_valid_protect_recovery_key( filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ), (int) $_GET['user_id'] ) ) { // phpcs:ignore: WordPress.Security.NonceVerification.Recommended -- no changes made if this isn't set.
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->valid_blocked_user_id = (int) $_GET['user_id']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if recovery key is valid.
|
||||
*
|
||||
* @param string $key - they recovery key.
|
||||
* @param int $user_id - the User ID.
|
||||
*/
|
||||
public function is_valid_protect_recovery_key( $key, $user_id ) {
|
||||
|
||||
$path = sprintf( '/sites/%d/protect/recovery/confirm', Jetpack_Options::get_option( 'id' ) );
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
$path,
|
||||
'1.1',
|
||||
array(
|
||||
'method' => 'post',
|
||||
),
|
||||
array(
|
||||
'token' => $key,
|
||||
'user_id' => $user_id,
|
||||
'ip' => $this->ip_address,
|
||||
)
|
||||
);
|
||||
|
||||
$result = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_transient( 'jetpack_protect_recovery_key_validated_' . $user_id, true, 600 );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should render the recovery form.
|
||||
*/
|
||||
public function render_and_die() {
|
||||
if ( ! $this->can_send_recovery_emails ) {
|
||||
$this->render_blocked_login_message();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_GET['validate_jetpack_protect_recovery'] ) && ! empty( $_GET['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes, just throws invalid token error.
|
||||
$error = new WP_Error( 'invalid_token', __( "Oops, we couldn't validate the recovery token.", 'jetpack-waf' ) );
|
||||
$this->protect_die( $error );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $_GET['jetpack-protect-recovery'] ) &&
|
||||
isset( $_POST['_wpnonce'] ) &&
|
||||
wp_verify_nonce( $_POST['_wpnonce'], 'bypass-protect' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP Core doesn't unstrip or sanitize nonces either.
|
||||
) {
|
||||
$this->process_recovery_email();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_GET['loggedout'] ) && 'true' === $_GET['loggedout'] ) {
|
||||
$this->protect_die( __( 'You successfully logged out.', 'jetpack-waf' ) );
|
||||
}
|
||||
|
||||
$this->render_recovery_form();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the blocked login message.
|
||||
*/
|
||||
public function render_blocked_login_message() {
|
||||
$this->protect_die( $this->get_html_blocked_login_message() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process sending a recovery email.
|
||||
*/
|
||||
public function process_recovery_email() {
|
||||
$sent = $this->send_recovery_email();
|
||||
$show_recovery_form = true;
|
||||
if ( is_wp_error( $sent ) ) {
|
||||
if ( 'email_already_sent' === $sent->get_error_code() ) {
|
||||
$show_recovery_form = false;
|
||||
}
|
||||
$this->protect_die( $sent, null, true, $show_recovery_form );
|
||||
} else {
|
||||
$this->render_recovery_success();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the recovery form.
|
||||
*/
|
||||
public function send_recovery_email() {
|
||||
$email = isset( $_POST['email'] ) ? wp_unslash( $_POST['email'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only triggered after bypass-protect nonce check is done, and sanitization is checked on the next line.
|
||||
if ( sanitize_email( $email ) !== $email || ! is_email( $email ) ) {
|
||||
return new WP_Error( 'invalid_email', __( "Oops, looks like that's not the right email address. Please try again!", 'jetpack-waf' ) );
|
||||
}
|
||||
$user = get_user_by( 'email', trim( $email ) );
|
||||
|
||||
if ( ! $user ) {
|
||||
return new WP_Error( 'invalid_user', __( "Oops, we couldn't find a user with that email. Please try again!", 'jetpack-waf' ) );
|
||||
}
|
||||
$this->email_address = $email;
|
||||
$path = sprintf( '/sites/%d/protect/recovery/request', Jetpack_Options::get_option( 'id' ) );
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
$path,
|
||||
'1.1',
|
||||
array(
|
||||
'method' => 'post',
|
||||
),
|
||||
array(
|
||||
'user_id' => $user->ID,
|
||||
'ip' => $this->ip_address,
|
||||
)
|
||||
);
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$result = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( self::HTTP_STATUS_CODE_TOO_MANY_REQUESTS === $code ) {
|
||||
// translators: email address the recovery instructions were sent to.
|
||||
return new WP_Error( 'email_already_sent', sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack-waf' ), esc_html( $this->email_address ) ) );
|
||||
} elseif ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
|
||||
return new WP_Error( 'email_send_error', __( 'Oops, we were unable to send a recovery email. Try again.', 'jetpack-waf' ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent login by locking the login page.
|
||||
*
|
||||
* @param string|WP_Error $content - the content of the page.
|
||||
* @param string|null $title - the page title.
|
||||
* @param bool $back_link - the back link.
|
||||
* @param bool $recovery_form - the recovery form.
|
||||
*/
|
||||
public function protect_die( $content, $title = null, $back_link = false, $recovery_form = false ) {
|
||||
if ( empty( $title ) ) {
|
||||
$title = __( 'Jetpack has locked your site\'s login page.', 'jetpack-waf' );
|
||||
}
|
||||
if ( is_wp_error( $content ) ) {
|
||||
$svg = '<svg class="gridicon gridicons-notice" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>';
|
||||
$content = '<span class="error"> ' . $svg . $content->get_error_message() . '</span>';
|
||||
}
|
||||
$content = '<p>' . $content . '</p>';
|
||||
|
||||
// If for some reason the login pop up box show up in the wp-admin.
|
||||
if ( isset( $_GET['interim-login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes to the site itself, just rendering an error message.
|
||||
$content = '<style>html{ background-color: #fff; } #error-message { margin:0 auto; padding: 1em; box-shadow: none; } </style>' . $content;
|
||||
}
|
||||
$this->display_page( $title, $content, $back_link, $recovery_form );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the recovery form.
|
||||
*/
|
||||
public function render_recovery_form() {
|
||||
$content = $this->get_html_blocked_login_message();
|
||||
$this->protect_die( $content, null, false, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the recovery instructions.
|
||||
*/
|
||||
public function render_recovery_success() {
|
||||
// translators: the email address the recovery email was sent to.
|
||||
$this->protect_die( sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack-waf' ), $this->email_address ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML for the blocked login message.
|
||||
*/
|
||||
public function get_html_blocked_login_message() {
|
||||
$icon = '<svg class="gridicon gridicons-spam" style="fill:#d94f4f" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M17 2H7L2 7v10l5 5h10l5-5V7l-5-5zm-4 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>';
|
||||
$ip = str_replace( 'http://', '', esc_url( 'http://' . $this->ip_address ) );
|
||||
return sprintf(
|
||||
// translators: the IP address that was flagged.
|
||||
__( '<p>Your IP address <code>%2$s</code> has been flagged for potential security violations. You can unlock your login by sending yourself a special link via email. <a href="%3$s">Learn More</a></p>', 'jetpack-waf' ), // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings
|
||||
$icon,
|
||||
$ip,
|
||||
esc_url( $this->get_help_url() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML recovery form.
|
||||
*/
|
||||
public function get_html_recovery_form() {
|
||||
// Build the markup as a string instead of using output buffering. Calling
|
||||
// ob_start() while PHP is executing an output-buffer handler (e.g. a page
|
||||
// cache or compression handler) raises a fatal: "Cannot use output buffering
|
||||
// in output buffering display handlers".
|
||||
return sprintf(
|
||||
'<div>
|
||||
<form method="post" action="?jetpack-protect-recovery=true">
|
||||
%1$s
|
||||
<p><label for="email">%2$s<br/></label>
|
||||
<input type="email" name="email" class="text-input"/>
|
||||
<input type="submit" class="button" value="%3$s"/>
|
||||
</p>
|
||||
</form>
|
||||
</div>',
|
||||
wp_nonce_field( 'bypass-protect', '_wpnonce', true, false ),
|
||||
esc_html__( 'Your email', 'jetpack-waf' ),
|
||||
esc_attr__( 'Send email', 'jetpack-waf' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the page.
|
||||
*
|
||||
* @param string $title - the page title.
|
||||
* @param string $message - the message we're sending.
|
||||
* @param bool $back_button - the back button.
|
||||
* @param bool $recovery_form - the recovery form.
|
||||
* @return never
|
||||
*/
|
||||
public function display_page( $title, $message, $back_button = false, $recovery_form = false ) {
|
||||
|
||||
if ( ! headers_sent() ) {
|
||||
nocache_headers();
|
||||
header( 'Content-Type: text/html; charset=utf-8' );
|
||||
}
|
||||
|
||||
$text_direction = 'ltr';
|
||||
if ( is_rtl() ) {
|
||||
$text_direction = 'rtl';
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
<?php
|
||||
if ( function_exists( 'language_attributes' ) && function_exists( 'is_rtl' ) ) {
|
||||
language_attributes();
|
||||
} else {
|
||||
echo "dir='$text_direction'"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
?>
|
||||
>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<?php
|
||||
if ( get_option( 'blog_public' ) ) {
|
||||
echo "<meta name='robots' content='noindex,follow' />\n";
|
||||
} else {
|
||||
echo "<meta name='robots' content='noindex,nofollow' />\n";
|
||||
}
|
||||
?>
|
||||
<title><?php echo esc_html( $title ); ?></title>
|
||||
<style type="text/css">
|
||||
html {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #2e4453;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
margin: 2em auto;
|
||||
padding: 1em 2em;
|
||||
max-width: 460px;
|
||||
text-align: left;
|
||||
}
|
||||
body.is-rtl {
|
||||
text-align: right;
|
||||
}
|
||||
h1 {
|
||||
clear: both;
|
||||
color: #3d596d;
|
||||
font-size: 24px;
|
||||
margin:0 0 24px 0;
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
box-sizing: border-box;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
#error-message img {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#error-page {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
#error-page p {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
|
||||
#error-page code {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
}
|
||||
|
||||
ul li {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00aadc;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
font-size:16px;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active {
|
||||
color: #0085be;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
color: #124964;
|
||||
-webkit-box-shadow: 0 0 0 1px #4f94d4,
|
||||
0 0 2px 1px rgba(30, 140, 190, .8);
|
||||
box-shadow: 0 0 0 1px #4f94d4,
|
||||
0 0 2px 1px rgba(30, 140, 190, .8);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #00aadc;
|
||||
color: white;
|
||||
border-color: #008ab3;
|
||||
border-style: solid;
|
||||
border-width: 1px 1px 2px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
margin-right: 0;
|
||||
outline: 0;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
text-decoration: none;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
border-radius: 4px;
|
||||
padding: 7px 14px 9px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
.button:focus {
|
||||
border-color: #005082;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
border-color: #005082;
|
||||
-webkit-box-shadow: 0 0 3px rgba(0, 115, 170, .8);
|
||||
box-shadow: 0 0 3px rgba(0, 115, 170, .8);
|
||||
outline: none;
|
||||
}
|
||||
.button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
border-width: 2px 1px 1px;
|
||||
}
|
||||
.gridicon {
|
||||
fill: currentColor;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#error-footer {
|
||||
padding: 16px;
|
||||
}
|
||||
#error-footer a {
|
||||
text-decoration: none;
|
||||
line-height:20px;
|
||||
font-size: 14px;
|
||||
color: #4f748e;
|
||||
}
|
||||
#error-footer a:hover {
|
||||
color: #2e4453;
|
||||
}
|
||||
#error-footer .gridicon{
|
||||
width: 16px;
|
||||
}
|
||||
#error-footer .gridicons-help {
|
||||
width: 24px;
|
||||
margin-right:8px;
|
||||
}
|
||||
|
||||
.is-rtl #error-footer .gridicons-help {
|
||||
margin-left:8px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #d94f4f;
|
||||
color:#FFF;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
line-height: 1.5;
|
||||
padding: 16px;
|
||||
padding-left: 42px;
|
||||
}
|
||||
.is-rtl .error {
|
||||
padding-right: 42px;
|
||||
}
|
||||
.error .gridicon {
|
||||
float: left;
|
||||
margin-left: -32px;
|
||||
}
|
||||
|
||||
.is-rtl .error .gridicon {
|
||||
float: right;
|
||||
margin-right: -32px;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
margin: 0;
|
||||
padding: 7px 14px;
|
||||
width: 100%;
|
||||
color: #2e4453;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #c8d7e1;
|
||||
background-color: white;
|
||||
transition: all .15s ease-in-out;
|
||||
box-sizing: border-box;
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
#image {
|
||||
display: block;
|
||||
width: 200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
<?php
|
||||
$rtl_class = '';
|
||||
if ( 'rtl' === $text_direction ) {
|
||||
$rtl_class = 'class="is-rtl"';
|
||||
echo 'body { font-family: Tahoma, Arial; }';
|
||||
}
|
||||
?>
|
||||
</style>
|
||||
</head>
|
||||
<body id="error-page" <?php echo $rtl_class; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<h1 id="error-title"><?php echo esc_html( $title ); ?></h1>
|
||||
<div id="error-message">
|
||||
<svg id="image" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 250 134">
|
||||
<path fill="#E9EFF4" d="M205.2,129.8c3.7-0.7,7.4-0.9,11.1-1.1l5.5-0.1l5.5,0c3.7,0,7.4,0.1,11.1,0.2c3.7,0.1,7.4,0.3,11.1,0.8 c0.3,0,0.5,0.3,0.5,0.6c0,0.2-0.2,0.4-0.5,0.5c-3.7,0.5-7.4,0.6-11.1,0.8c-3.7,0.1-7.4,0.2-11.1,0.2l-5.5,0l-5.5-0.1 c-3.7-0.1-7.4-0.4-11.1-1.1c-0.1,0-0.2-0.2-0.2-0.3C205,129.9,205.1,129.8,205.2,129.8"/>
|
||||
<path fill="#E9EFF4" d="M0.2,130.9c3-0.7,5.9-0.9,8.9-1.1l4.4-0.1l4.4,0c3,0,5.9,0.1,8.9,0.2c3,0.1,5.9,0.3,8.9,0.8 c0.3,0,0.5,0.3,0.4,0.6c0,0.2-0.2,0.4-0.4,0.4c-3,0.5-5.9,0.6-8.9,0.8c-3,0.1-5.9,0.2-8.9,0.2l-4.4,0l-4.4-0.1 c-3-0.1-5.9-0.4-8.9-1.1c-0.1,0-0.2-0.2-0.2-0.3C0,131,0.1,130.9,0.2,130.9"/>
|
||||
<path fill="#C8D7E2" d="M101.6,130.1H70.1V52.5c0-8.5,6.9-15.3,15.3-15.3h16.1V130.1z"/>
|
||||
<path fill="#0DA9DD" d="M191.5,130.1h-73.8v-5.4c0-8.9,7.2-16.1,16.1-16.1h57.7V130.1z"/>
|
||||
<path fill="#C7E9F5" d="M55.2,25.6l-0.1,9.8L55,57l-0.1,21.6c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4L56.6,57l0.8-21.6 c0.1-3.3,0.2-6.5,0.3-9.8H55.2z"/>
|
||||
<path fill="#C7E9F5" d="M203.1,25.6l0.1,18.1c0.2,28.8,0.4,57.6,1.2,86.3c0,0.4,0.4,0.8,0.8,0.8c0.4,0,0.8-0.3,0.8-0.8 c0.8-28.8,1-57.6,1.2-86.3l0.1-18.1H203.1z"/>
|
||||
<path fill="#7FD3F2" d="M55.3,25.6v-8.2v-6.8c0-5.9,4-10.7,9-10.7h134c5,0,9,4.8,9,10.7v14.9H55.3z"/>
|
||||
<path fill="#005083" d="M210.7,25.6c-13.3,1.1-26.7,1-40,1l-40,0.2l-40-0.2c-13.3-0.1-26.7,0-40-1V25c13.3-1.1,26.7-1,40-1l40-0.2 l40,0.2c13.3,0.1,26.7,0,40,1V25.6z"/>
|
||||
<polygon fill="#C7E9F5" points="168.7,95.6 117.7,95.6 117.7,44.6 "/>
|
||||
<path fill="#C8D7E2" d="M191.5,56.5c0,11-8.9,19.9-19.9,19.9c-11,0-19.9-8.9-19.9-19.9c0-11,8.9-19.9,19.9-19.9 C182.6,36.6,191.5,45.5,191.5,56.5"/>
|
||||
<path fill="#FFFFFF" d="M213.2,95.5c-3.3-5.1-3.2-16.7-3.2-28.4h-32.3c0,0-5.2,25.5,4.6,33c7.5-0.1,29.9-0.6,29.9-0.6"/>
|
||||
<path fill="#C8D7E2" d="M213.5,95.3l-0.1-0.1l-0.3-0.5c-0.2-0.4-0.3-0.7-0.5-1.1c-0.3-0.8-0.5-1.6-0.7-2.4c-0.1-0.5-0.2-1.1-0.3-1.6 c-0.4,0-0.8,0-1.2,0c0.5,2.1,1.1,4.3,2.4,6.1l0.2,0.2c0.2,0,0.4-0.1,0.5-0.3C213.6,95.5,213.6,95.4,213.5,95.3L213.5,95.3z"/>
|
||||
<path fill="#C8D7E2" d="M212.5,98.6c-0.1,0-0.2,0-0.3,0l-0.1,0H212l-0.3,0l-0.6,0l-1.3,0l-2.5,0l-5,0l-19.5,0.2 c-1.9-1.7-3.1-4.1-3.8-6.5c-0.8-2.6-1.1-5.4-1.2-8.2c-0.2-5.2,0.3-10.4,1.1-15.6l5.7-0.1c0-0.9,0-1.8,0-2.6l-4.4,0l-2.5,0 c-0.4,0-0.8,0.2-1,0.5c-0.1,0.2-0.2,0.3-0.3,0.5l-0.1,0.3l-0.2,1.2c-0.3,1.7-0.5,3.3-0.7,5c-0.3,3.3-0.5,6.7-0.4,10.1 c0.1,3.4,0.5,6.7,1.5,10c0.5,1.6,1.2,3.2,2.2,4.7c0.5,0.7,1,1.4,1.7,2c0.3,0.3,0.6,0.6,1,0.9l0.1,0.1c0.1,0,0.2,0.1,0.3,0.2 c0.2,0.1,0.5,0.1,0.6,0.1l0.6,0l20-0.6l5-0.2l2.5-0.1l1.2,0l0.3,0l0.2,0c0,0,0.3,0,0.4-0.1c0.3-0.2,0.5-0.5,0.5-0.9 C213.1,99.1,212.9,98.7,212.5,98.6z"/>
|
||||
<path fill="#FFFFFF" d="M223.1,84.8c-3.3-5.1-4.8-16.7-4.8-28.4h-32.3c0,0-3.5,25.5,6.3,33c7.5-0.1,29.9-0.6,29.9-0.6"/>
|
||||
<path fill="#C8D7E2" d="M222.9,84.9c-1.3-2.1-2.2-4.4-2.8-6.7c-0.6-2.4-1.1-4.8-1.5-7.2c-0.7-4.8-1-9.1-1-13.9l0,0l-31,0.1l0,0 c-0.4,2.8-0.5,5.1-0.5,7.9c-0.1,2.9,0,5.7,0.3,8.6c0.3,2.8,0.8,5.7,1.7,8.3c0.9,2.6,2.3,5.2,4.5,6.9l-0.4-0.1l14.9-0.2 c5-0.1,10-0.1,14.9-0.1c0.1,0,0.3,0.1,0.3,0.3c0,0.1-0.1,0.3-0.2,0.3c-5,0.2-10,0.4-14.9,0.5l-14.9,0.4c-0.1,0-0.3,0-0.4-0.1l0,0 c-2.5-1.9-3.9-4.7-5-7.4c-1-2.8-1.5-5.7-1.9-8.6c-0.3-2.9-0.4-5.8-0.4-8.8c0.1-2.9,0.2-5.8,0.6-8.8c0-0.4,0.4-0.6,0.7-0.6h0 l32.3,0.1h0c0.3,0,0.6,0.3,0.6,0.6v0c0,4.8,0.2,9.6,0.7,14.4c0.3,2.4,0.6,4.8,1.2,7.1c0.5,2.3,1.2,4.7,2.4,6.8c0,0.1,0,0.1,0,0.2 C223.1,85,223,85,222.9,84.9"/>
|
||||
<path fill="#C8D7E2" d="M192.1,67.1c1.6-0.9,3.4-1.2,5.1-1.3c1.7-0.2,3.5-0.2,5.2-0.2c3.5,0.1,6.9,0.2,10.3,1c0.1,0,0.2,0.2,0.2,0.3 c0,0.1-0.1,0.2-0.2,0.2c-3.4,0.2-6.9,0-10.3,0c-1.7,0-3.4,0-5.1,0c-1.7,0-3.4,0.1-5.1,0.3l0,0c-0.1,0-0.1,0-0.1-0.1 C192,67.2,192.1,67.1,192.1,67.1"/>
|
||||
<path fill="#C8D7E2" d="M194.1,74c1.4,0,2.7,0,4.1,0c1.4,0,2.7,0,4.1,0c2.7,0,5.4-0.1,8.2-0.2c0.1,0,0.3,0.1,0.3,0.3 c0,0.1-0.1,0.2-0.2,0.3c-1.3,0.5-2.7,0.7-4.1,0.9c-1.4,0.2-2.8,0.2-4.2,0.3c-1.4,0-2.8,0-4.2-0.2c-1.4-0.2-2.8-0.4-4.1-1.1 c-0.1,0-0.1-0.1,0-0.2C193.9,74.1,194,74,194.1,74L194.1,74z"/>
|
||||
<path fill="#86A6BD" d="M40.2,88.6c-0.5,0-0.8-0.4-0.9-0.9l-0.1-8.2c0-0.7,0-1.4,0-2.1c0.1-0.7,0.2-1.5,0.4-2.2c0.4-1.4,1-2.8,1.9-4 c1.7-2.5,4.3-4.3,7.1-5.1c0.7-0.2,1.5-0.3,2.2-0.5c0.7-0.1,1.5-0.1,2.2-0.1c1.3,0,2.9,0,4.4,0.4c2.9,0.7,5.6,2.5,7.4,4.9 c0.9,1.2,1.6,2.6,2.1,4c0.5,1.4,0.6,3,0.6,4.4l0,16.4c0,0.7-0.6,1.3-1.3,1.3l-6.7,0c-0.7,0-1.3-0.6-1.3-1.3v0l0-10.8l0-5.4 c0-1.4-0.7-2.8-1.8-3.5c-0.6-0.4-1.3-0.6-2-0.7c-0.7,0-1.9,0-2.5,0c-1.4,0.1-2.7,1-3.3,2.3c-0.3,0.7-0.4,1.3-0.4,2.1l0,2.7 l-0.1,5.4l0,0c0,0.5-0.4,0.9-1,0.9"/>
|
||||
<path fill="#FFFFFF" d="M41.1,86.9l0.1-7.3c-0.1-2.6,0.7-5,2.1-7.1c1.4-2,3.6-3.5,5.9-4.1c0.6-0.2,1.2-0.3,1.8-0.3 c0.6,0,1.2-0.1,1.9,0c1.4,0,2.5,0,3.7,0.4c2.4,0.6,4.5,2,5.9,4c0.7,1,1.3,2.1,1.6,3.2c0.4,1.2,0.5,2.3,0.5,3.7l0,15.1l0,0l-4.2,0 l0-9.5l0-5.4c0-2.2-1.2-4.4-3-5.5c-0.9-0.6-2-0.9-3.1-1c-1.1,0-1.7,0-2.9,0c-2.2,0.2-4.2,1.7-5.1,3.6c-0.5,0.9-0.7,2.1-0.6,3.1 l0,2.7l0.1,4.4l0,0L41.1,86.9L41.1,86.9"/>
|
||||
<path fill="#86A6BD" d="M36.3,133c-1.9,0-3.8-1.1-4.8-2.8c-0.5-0.8-0.7-1.8-0.7-2.8l0-2.4l0-9.6l-0.1-9.6l0-4.8c0-0.7,0-1.8,0.3-2.8 c0.3-1,0.9-1.8,1.7-2.5c0.8-0.6,1.7-1.1,2.7-1.3c1.1-0.2,1.8-0.1,2.6-0.1l4.8,0l9.6-0.1l19.2,0c2.1,0,4.1,1.2,5.1,3 c0.5,0.9,0.8,2,0.8,3l0,2.4l0,9.6l-0.1,9.6l0,4.8c0,0.7,0,1.8-0.4,2.8c-0.3,0.9-1,1.8-1.7,2.4c-0.8,0.6-1.7,1.1-2.7,1.2 c-1.1,0.1-1.8,0-2.6,0.1l-4.8,0l-9.6-0.1L36.3,133z"/>
|
||||
<path fill="#FFFFFF" d="M74.8,112.3l-0.1-9.6l0-2.4c0-0.6-0.1-1.1-0.4-1.6c-0.6-1-1.7-1.6-2.8-1.6l-19.2,0L42.7,97l-4.8,0 c-0.8,0-1.7,0-2.2,0c-0.6,0.1-1.1,0.3-1.6,0.7c-0.5,0.4-0.8,0.9-1,1.4c-0.2,0.6-0.2,1.1-0.2,2l0,4.8l-0.1,9.6l0,9.6l0,2.4 c0,0.6,0.2,1.3,0.5,1.8c0.6,1.1,1.9,1.8,3.1,1.8l19.2-0.1l9.6-0.1l4.8,0c0.8,0,1.7,0,2.2-0.1c0.6-0.1,1.2-0.4,1.6-0.8 c0.5-0.4,0.8-0.9,1-1.5c0.2-0.6,0.2-1.1,0.2-2l0-4.8L74.8,112.3z"/>
|
||||
<path fill="#86A6BD" d="M48.1,121.4l2.9-6.2c0.3-0.6,0.2-1.3-0.3-1.8c-1-1-1.5-2.5-1.2-4c0.3-1.7,1.7-3.1,3.4-3.4 c2.9-0.6,5.4,1.6,5.4,4.4c0,1.2-0.5,2.3-1.3,3.1c-0.5,0.5-0.6,1.2-0.3,1.8l2.9,6.2c0.1,0.2-0.1,0.5-0.3,0.5H48.4 C48.1,121.9,48,121.6,48.1,121.4"/>
|
||||
</svg>
|
||||
|
||||
<?php echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- message includes HTML that's marked up ourselves. ?>
|
||||
<?php
|
||||
if ( $recovery_form ) {
|
||||
echo $this->get_html_recovery_form(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- content is escaped in the function.
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div id="error-footer">
|
||||
<?php
|
||||
if ( $back_button && ! $recovery_form ) {
|
||||
if ( 'rtl' === $text_direction ) {
|
||||
$back_button_icon = '<svg class="gridicon gridicons-arrow-right" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></g></svg>';
|
||||
} else {
|
||||
$back_button_icon = '<svg class="gridicon gridicons-arrow-left" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></g></svg>';
|
||||
}
|
||||
?>
|
||||
<a href='javascript:history.back()'
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s is HTML markup, for a back icon. */
|
||||
__( '%s Back', 'jetpack-waf' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
$back_button_icon // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
);
|
||||
?>
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
$help_icon = '<svg class="gridicon gridicons-help" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 16h-2v-2h2v2zm0-4.14V15h-2v-2c0-.552.448-1 1-1 1.103 0 2-.897 2-2s-.897-2-2-2-2 .897-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.862-1.278 3.413-3 3.86z"/></g></svg>';
|
||||
?>
|
||||
<a href="<?php echo esc_url( $this->get_help_url() ); ?>" rel="noopener noreferrer" target="_blank">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s is HTML markup, for a help icon. */
|
||||
__( '%s Get help unlocking your site', 'jetpack-waf' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
$help_icon // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
);
|
||||
?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
die( 0 );
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Class used to define Brute Force Protection Blocked Login Page.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf\Brute_Force_Protection;
|
||||
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Waf\Blocked_Login_Page;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Brute Force Protection Blocked Login Page class.
|
||||
*/
|
||||
class Brute_Force_Protection_Blocked_Login_Page extends Blocked_Login_Page {
|
||||
|
||||
/**
|
||||
* Instance of the class.
|
||||
*
|
||||
* @var Brute_Force_Protection_Blocked_Login_Page
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Instance of the class.
|
||||
*
|
||||
* @param string $ip_address IP address.
|
||||
*
|
||||
* @return Brute_Force_Protection_Blocked_Login_Page
|
||||
*/
|
||||
public static function instance( $ip_address ) {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self( $ip_address );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the help URL for Brute Force Protection.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_help_url() {
|
||||
return Redirect::get_url( 'jetpack-support-jetpack-waf', array( 'anchor' => 'troubleshooting' ) );
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
namespace Automattic\Jetpack\Waf\Brute_Force_Protection;
|
||||
|
||||
if ( ! class_exists( 'Brute_Force_Protection_Math_Authenticate' ) ) {
|
||||
|
||||
/**
|
||||
* The math captcha fallback if we can't talk to the Protect API
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class Brute_Force_Protection_Math_Authenticate {
|
||||
|
||||
/**
|
||||
* If the class is loaded.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $loaded;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
if ( self::$loaded ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$loaded = 1;
|
||||
|
||||
add_action( 'login_form', array( $this, 'math_form' ) );
|
||||
|
||||
if ( isset( $_POST['jetpack_protect_process_math_form'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- No changes made, just queues the math authenticator hook.
|
||||
add_action( 'init', array( $this, 'process_generate_math_page' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The timeout window.
|
||||
*/
|
||||
private static function time_window() {
|
||||
return ceil( time() / ( MINUTE_IN_SECONDS * 2 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a user answered the math problem correctly while logging in.
|
||||
*
|
||||
* @return bool Returns true if the math is correct. Exits if not.
|
||||
*/
|
||||
public static function math_authenticate() {
|
||||
if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
|
||||
$brute_force_protection = Brute_Force_Protection::instance();
|
||||
$transient = $brute_force_protection->get_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
|
||||
|
||||
if ( ! $transient || $transient < 1 ) {
|
||||
self::generate_math_page();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$ans = isset( $_POST['jetpack_protect_num'] ) ? (int) $_POST['jetpack_protect_num'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- answers are salted.
|
||||
$correct_ans = isset( $_POST['jetpack_protect_answer'] ) ? sanitize_key( $_POST['jetpack_protect_answer'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
|
||||
$time_window = self::time_window();
|
||||
$salt = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
|
||||
$salted_ans_1 = hash_hmac( 'sha1', $ans, $salt . $time_window );
|
||||
$salted_ans_2 = hash_hmac( 'sha1', $ans, $salt . ( $time_window - 1 ) );
|
||||
|
||||
if ( ! $correct_ans || ! $ans ) {
|
||||
self::generate_math_page();
|
||||
} elseif ( ! hash_equals( $salted_ans_1, $correct_ans ) && ! hash_equals( $salted_ans_2, $correct_ans ) ) {
|
||||
wp_die(
|
||||
wp_kses(
|
||||
__(
|
||||
'<strong>You failed to correctly answer the math problem.</strong> This is used to combat spam when Jetpack’s Brute Force Attack Protection API is unavailable. Please use your browser’s back button to return to the login form, press the "refresh" button to generate a new math problem, and try to log in again.',
|
||||
'jetpack-waf'
|
||||
),
|
||||
array( 'strong' => array() )
|
||||
),
|
||||
'',
|
||||
array( 'response' => 401 )
|
||||
);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interim page to collect answers to a math captcha
|
||||
*
|
||||
* @param string $error - the error message.
|
||||
* @return never
|
||||
*/
|
||||
public static function generate_math_page( $error = false ) {
|
||||
ob_start();
|
||||
?>
|
||||
<h2><?php esc_html_e( 'Please solve this math problem to prove that you are not a bot. Once you solve it, you will need to log in again.', 'jetpack-waf' ); ?></h2>
|
||||
<?php if ( $error ) : ?>
|
||||
<h3><?php esc_html_e( 'Your answer was incorrect, please try again.', 'jetpack-waf' ); ?></h3>
|
||||
<?php endif ?>
|
||||
|
||||
<form action="<?php echo esc_url( wp_login_url() ); ?>" method="post" accept-charset="utf-8">
|
||||
<?php self::math_form(); ?>
|
||||
<input type="hidden" name="jetpack_protect_process_math_form" value="1" id="jetpack_protect_process_math_form" />
|
||||
<p><input type="submit" value="<?php esc_attr_e( 'Continue →', 'jetpack-waf' ); ?>"></p>
|
||||
</form>
|
||||
<?php
|
||||
$mathpage = ob_get_contents();
|
||||
ob_end_clean();
|
||||
wp_die(
|
||||
$mathpage, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- content is escaped.
|
||||
'',
|
||||
array( 'response' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the math page.
|
||||
*/
|
||||
public function process_generate_math_page() {
|
||||
$ans = isset( $_POST['jetpack_protect_num'] ) ? (int) $_POST['jetpack_protect_num'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- answers are salted.
|
||||
$correct_ans = isset( $_POST['jetpack_protect_answer'] ) ? sanitize_key( $_POST['jetpack_protect_answer'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
|
||||
$time_window = self::time_window();
|
||||
$salt = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
|
||||
$salted_ans_1 = hash_hmac( 'sha1', $ans, $salt . $time_window );
|
||||
$salted_ans_2 = hash_hmac( 'sha1', $ans, $salt . ( $time_window - 1 ) );
|
||||
|
||||
if ( ! hash_equals( $salted_ans_1, $correct_ans ) && ! hash_equals( $salted_ans_2, $correct_ans ) ) {
|
||||
self::generate_math_page( true );
|
||||
} else {
|
||||
$temp_pass = substr( hash_hmac( 'sha1', (string) wp_rand( 1, 100000000 ), get_site_option( 'jetpack_protect_key' ) ), 5, 25 );
|
||||
|
||||
$brute_force_protection = Brute_Force_Protection::instance();
|
||||
$brute_force_protection->set_transient( 'jpp_math_pass_' . $temp_pass, 3, DAY_IN_SECONDS );
|
||||
setcookie( 'jpp_math_pass', $temp_pass, time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
|
||||
remove_action( 'login_form', array( $this, 'math_form' ) );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires a user to solve a simple equation. Added to any WordPress login form.
|
||||
*
|
||||
* @return VOID outputs html
|
||||
*/
|
||||
public static function math_form() {
|
||||
// Check if jpp_math_pass cookie is set and it matches valid transient.
|
||||
if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
|
||||
$brute_force_protection = Brute_Force_Protection::instance();
|
||||
$transient = $brute_force_protection->get_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
|
||||
|
||||
if ( $transient && $transient > 0 ) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
$num1 = wp_rand( 0, 10 );
|
||||
$num2 = wp_rand( 1, 10 );
|
||||
$ans = $num1 + $num2;
|
||||
|
||||
$time_window = self::time_window();
|
||||
$salt = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
|
||||
$salted_ans = hash_hmac( 'sha1', (string) $ans, $salt . $time_window );
|
||||
?>
|
||||
<div style="margin: 5px 0 20px;">
|
||||
<p style="font-size: 14px;">
|
||||
<?php esc_html_e( 'Prove your humanity', 'jetpack-waf' ); ?>
|
||||
</p>
|
||||
<br/>
|
||||
<label for="jetpack_protect_answer" style="vertical-align:super;">
|
||||
<?php echo esc_html( "$num1 + $num2 = " ); ?>
|
||||
</label>
|
||||
<input type="number" id="jetpack_protect_answer" name="jetpack_protect_num" value="" size="2" style="width:50px;height:25px;vertical-align:middle;font-size:13px;" class="input" />
|
||||
<input type="hidden" name="jetpack_protect_answer" value="<?php echo esc_attr( $salted_ans ); ?>" />
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for functions shared by the Brute force protection feature and its related json-endpoints
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf\Brute_Force_Protection;
|
||||
|
||||
use Automattic\Jetpack\IP\Utils as IP_Utils;
|
||||
use Automattic\Jetpack\Waf\Waf_Rules_Manager;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Shared Functions class.
|
||||
*/
|
||||
class Brute_Force_Protection_Shared_Functions {
|
||||
/**
|
||||
* Returns an array of IP objects that will never be blocked by the Brute force protection feature
|
||||
*
|
||||
* @deprecated 0.11.0 Use format_allow_list()
|
||||
*/
|
||||
public static function format_whitelist() {
|
||||
_deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::format_allow_list' );
|
||||
return self::format_allow_list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of IP objects that will never be blocked by the Brute force protection feature
|
||||
*
|
||||
* The array is segmented into a local allow list which applies only to the current site
|
||||
* and a global allow list which, for multisite installs, applies to the entire networko
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function format_allow_list() {
|
||||
$local_allow_list = self::get_local_allow_list();
|
||||
$formatted = array(
|
||||
'local' => array(),
|
||||
);
|
||||
foreach ( $local_allow_list as $item ) {
|
||||
if ( $item->range ) {
|
||||
$formatted['local'][] = $item->range_low . ' - ' . $item->range_high;
|
||||
} else {
|
||||
$formatted['local'][] = $item->ip_address;
|
||||
}
|
||||
}
|
||||
if ( is_multisite() && current_user_can( 'manage_network' ) ) {
|
||||
$formatted['global'] = array();
|
||||
$global_allow_list = self::get_global_allow_list();
|
||||
if ( false === $global_allow_list ) {
|
||||
// If the global allow list has never been set, check for a legacy option set prior to 3.6.
|
||||
$global_allow_list = get_site_option( 'jetpack_protect_whitelist', array() );
|
||||
}
|
||||
foreach ( $global_allow_list as $item ) {
|
||||
if ( $item->range ) {
|
||||
$formatted['global'][] = $item->range_low . ' - ' . $item->range_high;
|
||||
} else {
|
||||
$formatted['global'][] = $item->ip_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the local Brute force protection allow list.
|
||||
*
|
||||
* @deprecated 0.11.0 Use get_local_allow_list()
|
||||
*/
|
||||
public static function get_local_whitelist() {
|
||||
_deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::get_local_allow_list' );
|
||||
return self::get_local_allow_list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the local Brute force protection allow list.
|
||||
*
|
||||
* The 'local' part of the allow list only really applies to multisite installs,
|
||||
* which can have a network wide allow list, as well as a local list that applies
|
||||
* only to the current site. On single site installs, there will only be a local
|
||||
* allow list.
|
||||
*
|
||||
* @return array A list of IP Address objects or an empty array
|
||||
*/
|
||||
public static function get_local_allow_list() {
|
||||
$allow_list = get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME );
|
||||
if ( false === $allow_list ) {
|
||||
// The local allow list has never been set.
|
||||
if ( is_multisite() ) {
|
||||
// On a multisite, we can check for a legacy site_option that existed prior to v 3.6, or default to an empty array.
|
||||
$allow_list = get_site_option( 'jetpack_protect_whitelist', array() );
|
||||
} else {
|
||||
// On a single site, we can just use an empty array.
|
||||
$allow_list = array();
|
||||
}
|
||||
} else {
|
||||
$allow_list = IP_Utils::get_ip_addresses_from_string( $allow_list );
|
||||
$allow_list = array_map(
|
||||
function ( $ip_address ) {
|
||||
return self::create_ip_object( $ip_address );
|
||||
},
|
||||
$allow_list
|
||||
);
|
||||
}
|
||||
return $allow_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global, network-wide allow list.
|
||||
*
|
||||
* @deprecated 0.11.0 Use get_global_allow_list()
|
||||
*/
|
||||
public static function get_global_whitelist() {
|
||||
_deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::get_global_allow_list' );
|
||||
return self::get_global_allow_list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global, network-wide allow list.
|
||||
*
|
||||
* It will revert to the legacy site_option if jetpack_protect_global_whitelist has never been set.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_global_allow_list() {
|
||||
$allow_list = get_site_option( 'jetpack_protect_global_whitelist' );
|
||||
if ( false === $allow_list ) {
|
||||
// The global allow list has never been set. Check for legacy site_option, or default to an empty array.
|
||||
$allow_list = get_site_option( 'jetpack_protect_whitelist', array() );
|
||||
}
|
||||
return $allow_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string into an IP Address object.
|
||||
*
|
||||
* @param string $ip_address The IP Address to convert.
|
||||
* @return object An IP Address object.
|
||||
*/
|
||||
private static function create_ip_object( $ip_address ) {
|
||||
// Hyphenated range notation.
|
||||
if ( strpos( $ip_address, '-' ) ) {
|
||||
$ip_range_parts = explode( '-', $ip_address );
|
||||
return (object) array(
|
||||
'range' => true,
|
||||
'range_low' => trim( $ip_range_parts[0] ),
|
||||
'range_high' => trim( $ip_range_parts[1] ),
|
||||
);
|
||||
}
|
||||
|
||||
// CIDR notation.
|
||||
if ( strpos( $ip_address, '/' ) !== false ) {
|
||||
return (object) array(
|
||||
'range' => true,
|
||||
'range_low' => $ip_address,
|
||||
'range_high' => null,
|
||||
);
|
||||
}
|
||||
|
||||
// Single IP Address.
|
||||
return (object) array(
|
||||
'range' => false,
|
||||
'ip_address' => $ip_address,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save IP allow list.
|
||||
*
|
||||
* @deprecated 0.11.0 Use save_allow_list()
|
||||
*
|
||||
* @param mixed $allow_list IP allow list.
|
||||
* @param bool $global (default: false) Global.
|
||||
*/
|
||||
public static function save_whitelist( $allow_list, $global = false ) {
|
||||
_deprecated_function( __METHOD__, 'waf-0.11.0', __CLASS__ . '::save_allow_list' );
|
||||
return self::save_allow_list( $allow_list, $global );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save IP allow list.
|
||||
*
|
||||
* @access public
|
||||
* @param mixed $allow_list IP allow list.
|
||||
* @param bool $global (default: false) Global.
|
||||
* @return bool
|
||||
*/
|
||||
public static function save_allow_list( $allow_list, $global = false ) {
|
||||
$allow_list_error = false;
|
||||
$new_items = array();
|
||||
if ( ! is_array( $allow_list ) ) {
|
||||
return new WP_Error( 'invalid_parameters', __( 'Expecting an array', 'jetpack-waf' ) );
|
||||
}
|
||||
if ( $global && ! is_multisite() ) {
|
||||
return new WP_Error( 'invalid_parameters', __( 'Cannot use global flag on non-multisites', 'jetpack-waf' ) );
|
||||
}
|
||||
if ( $global && ! current_user_can( 'manage_network' ) ) {
|
||||
return new WP_Error( 'permission_denied', __( 'Only super admins can edit the global allow list', 'jetpack-waf' ) );
|
||||
}
|
||||
// Validate each item.
|
||||
foreach ( $allow_list as $item ) {
|
||||
$item = trim( $item );
|
||||
if ( empty( $item ) ) {
|
||||
continue;
|
||||
}
|
||||
$new_item = self::create_ip_object( $item );
|
||||
if ( $new_item->range ) {
|
||||
if ( ! filter_var( $new_item->range_low, FILTER_VALIDATE_IP ) || ! filter_var( $new_item->range_high, FILTER_VALIDATE_IP ) ) {
|
||||
$allow_list_error = true;
|
||||
break;
|
||||
}
|
||||
if ( ! IP_Utils::convert_ip_address( $new_item->range_low ) || ! IP_Utils::convert_ip_address( $new_item->range_high ) ) {
|
||||
$allow_list_error = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if ( ! filter_var( $new_item->ip_address, FILTER_VALIDATE_IP ) ) {
|
||||
$allow_list_error = true;
|
||||
break;
|
||||
}
|
||||
if ( ! IP_Utils::convert_ip_address( $new_item->ip_address ) ) {
|
||||
$allow_list_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$new_items[] = $new_item;
|
||||
} // End item loop.
|
||||
if ( ! empty( $allow_list_error ) ) {
|
||||
return new WP_Error( 'invalid_ip', __( 'One of your IP addresses was not valid.', 'jetpack-waf' ) );
|
||||
}
|
||||
if ( $global ) {
|
||||
update_site_option( 'jetpack_protect_global_whitelist', $new_items );
|
||||
// Once a user has saved their global allow list, we can permanently remove the legacy option.
|
||||
delete_site_option( 'jetpack_protect_whitelist' );
|
||||
} else {
|
||||
$new_items = array_map(
|
||||
function ( $item ) {
|
||||
if ( $item->range ) {
|
||||
return $item->range_low . '-' . $item->range_high;
|
||||
}
|
||||
return $item->ip_address;
|
||||
},
|
||||
$new_items
|
||||
);
|
||||
update_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME, implode( ' ', $new_items ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Adapted from Purge Transients by Seebz
|
||||
* https://github.com/Seebz/Snippets/tree/master/Wordpress/plugins/purge-transients
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf\Brute_Force_Protection;
|
||||
|
||||
/**
|
||||
* Transient Cleanup class.
|
||||
*/
|
||||
class Brute_Force_Protection_Transient_Cleanup {
|
||||
/**
|
||||
* Jetpack Purge Transients.
|
||||
*
|
||||
* @access public
|
||||
* @param string $older_than (default: '1 hour') Older Than.
|
||||
*/
|
||||
public static function jp_purge_transients( $older_than = '1 hour' ) {
|
||||
global $wpdb;
|
||||
$older_than_time = strtotime( '-' . $older_than );
|
||||
if ( $older_than_time > time() || $older_than_time < 1 ) {
|
||||
return false;
|
||||
}
|
||||
$sql = $wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery
|
||||
"SELECT REPLACE(option_name, '_transient_timeout_jpp_', '') AS transient_name FROM {$wpdb->options} WHERE option_name LIKE '\_transient\_timeout\_jpp\__%%' AND option_value < %d",
|
||||
$older_than_time
|
||||
);
|
||||
$transients = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB -- $sql is prepared above.
|
||||
$options_names = array();
|
||||
foreach ( $transients as $transient ) {
|
||||
$options_names[] = '_transient_jpp_' . $transient;
|
||||
$options_names[] = '_transient_timeout_jpp_' . $transient;
|
||||
}
|
||||
if ( $options_names ) {
|
||||
$option_names_string = implode( ', ', array_fill( 0, count( $options_names ), '%s' ) );
|
||||
$result = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name IN ($option_names_string)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- the placeholders are set above.
|
||||
$options_names
|
||||
)
|
||||
);
|
||||
if ( ! $result ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jetpack Purge Transients Activation.
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public static function jp_purge_transients_activation() {
|
||||
if ( ! wp_next_scheduled( 'jp_purge_transients_cron' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'jp_purge_transients_cron' );
|
||||
}
|
||||
}
|
||||
}
|
||||
+1199
File diff suppressed because it is too large
Load Diff
+331
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* Class used to manage backwards-compatibility of the package.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Defines methods for ensuring backwards compatibility.
|
||||
*/
|
||||
class Waf_Compatibility {
|
||||
|
||||
/**
|
||||
* Returns the name for the IP allow list enabled/disabled option.
|
||||
*
|
||||
* @since 0.22.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_ip_allow_list_enabled_option_name() {
|
||||
/**
|
||||
* Patch: bootstrap script generated prior to 0.17.0 may have autoloaded Waf_Rules_Manager class during standalone mode execution.
|
||||
*
|
||||
* @see peb6dq-2HL-p2
|
||||
*/
|
||||
if ( ! defined( 'Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME' ) ) {
|
||||
return 'jetpack_waf_ip_allow_list_enabled';
|
||||
}
|
||||
|
||||
return Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name for the IP block list enabled/disabled option.
|
||||
*
|
||||
* @since 0.22.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_ip_block_list_enabled_option_name() {
|
||||
/**
|
||||
* Patch: bootstrap script generated prior to 0.17.0 may have autoloaded Waf_Rules_Manager class during standalone mode execution.
|
||||
*
|
||||
* @see peb6dq-2HL-p2
|
||||
*/
|
||||
if ( ! defined( 'Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME' ) ) {
|
||||
return 'jetpack_waf_ip_block_list_enabled';
|
||||
}
|
||||
|
||||
return Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add compatibilty hooks
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_compatibility_hooks() {
|
||||
add_filter( 'default_option_' . Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME, __CLASS__ . '::default_option_waf_automatic_rules', 10, 3 );
|
||||
add_filter( 'default_option_' . Waf_Initializer::NEEDS_UPDATE_OPTION_NAME, __CLASS__ . '::default_option_waf_needs_update', 10, 3 );
|
||||
add_filter( 'default_option_' . Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME, __CLASS__ . '::default_option_waf_ip_allow_list', 10, 3 );
|
||||
add_filter( 'option_' . Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME, __CLASS__ . '::filter_option_waf_ip_allow_list', 10, 1 );
|
||||
add_filter( 'default_option_' . self::get_ip_allow_list_enabled_option_name(), __CLASS__ . '::default_option_waf_ip_allow_list_enabled', 10, 3 );
|
||||
add_filter( 'default_option_' . self::get_ip_block_list_enabled_option_name(), __CLASS__ . '::default_option_waf_ip_block_list_enabled', 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run compatibility migrations.
|
||||
*
|
||||
* Note that this method should be compatible with sites where
|
||||
* the request firewall is not active or not supported.
|
||||
*
|
||||
* @see Waf_Runner::is_supported_environment().
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function run_compatibility_migrations() {
|
||||
self::migrate_brute_force_protection_ip_allow_list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a default value for sites that installed the WAF
|
||||
* before the automatic rules option was introduced.
|
||||
*
|
||||
* @since 0.9.0
|
||||
*
|
||||
* @param mixed $default The default value to return if the option does not exist in the database.
|
||||
* @param string $option Option name.
|
||||
* @param bool $passed_default Was get_option() passed a default value.
|
||||
*
|
||||
* @return mixed The default value to return if the option does not exist in the database.
|
||||
*/
|
||||
public static function default_option_waf_automatic_rules( $default, $option, $passed_default ) {
|
||||
// Allow get_option() to override this default value
|
||||
if ( $passed_default ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return self::get_default_automatic_rules_option();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the option is not available, use the WAF module status
|
||||
* to determine whether or not to run automatic rules.
|
||||
*
|
||||
* @since 0.9.0
|
||||
*
|
||||
* @return bool The default value for automatic rules.
|
||||
*/
|
||||
public static function get_default_automatic_rules_option() {
|
||||
return Waf_Runner::is_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a default value for sites that installed the WAF
|
||||
* before the NEEDS_UPDATE_OPTION_NAME option was added.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @param mixed $default The default value to return if the option does not exist in the database.
|
||||
* @param string $option Option name.
|
||||
* @param bool $passed_default Was get_option() passed a default value.
|
||||
*
|
||||
* @return mixed The default value to return if the option does not exist in the database.
|
||||
*/
|
||||
public static function default_option_waf_needs_update( $default, $option, $passed_default ) {
|
||||
// Allow get_option() to override this default value
|
||||
if ( $passed_default ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// If the option hasn't been added yet, the WAF needs to be updated.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the WAF and Brute Force Protection IP allow lists.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @param string $waf_allow_list The WAF IP allow list.
|
||||
* @param array $brute_force_allow_list The Brute Force Protection IP allow list. Array of IP objects.
|
||||
*
|
||||
* @return string The merged IP allow list.
|
||||
*/
|
||||
public static function merge_ip_allow_lists( $waf_allow_list, $brute_force_allow_list ) {
|
||||
|
||||
// Drop malformed entries.
|
||||
$brute_force_allow_list = is_array( $brute_force_allow_list ) ? array_filter( $brute_force_allow_list, 'is_object' ) : array();
|
||||
|
||||
if ( empty( $brute_force_allow_list ) ) {
|
||||
return $waf_allow_list;
|
||||
}
|
||||
|
||||
// Convert the IP objects to strings.
|
||||
$brute_force_allow_list = array_map(
|
||||
function ( $ip_object ) {
|
||||
if ( ! empty( $ip_object->range ) ) {
|
||||
return $ip_object->range_low . '-' . $ip_object->range_high;
|
||||
}
|
||||
|
||||
return $ip_object->ip_address;
|
||||
},
|
||||
$brute_force_allow_list
|
||||
);
|
||||
|
||||
$brute_force_allow_list_string = implode( "\n", $brute_force_allow_list );
|
||||
|
||||
if ( empty( $waf_allow_list ) ) {
|
||||
return $brute_force_allow_list_string;
|
||||
}
|
||||
|
||||
// Return the lists merged into a single string.
|
||||
return "$waf_allow_list\n$brute_force_allow_list_string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the brute force protection IP allow list option to the WAF option.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function migrate_brute_force_protection_ip_allow_list() {
|
||||
// Get the allow list values directly from the database to avoid filters.
|
||||
$brute_force_allow_list = Jetpack_Options::get_raw_option( 'jetpack_protect_whitelist' );
|
||||
$waf_allow_list = Jetpack_Options::get_raw_option( 'jetpack_waf_ip_allow_list' );
|
||||
|
||||
if ( ! empty( $brute_force_allow_list ) ) {
|
||||
|
||||
if ( empty( $waf_allow_list ) ) {
|
||||
$waf_allow_list = '';
|
||||
}
|
||||
|
||||
// Merge the two allow lists.
|
||||
$merged_allow_list = self::merge_ip_allow_lists( $waf_allow_list, $brute_force_allow_list );
|
||||
|
||||
// Update the WAF IP allow list with the merged list.
|
||||
Jetpack_Options::update_raw_option( 'jetpack_waf_ip_allow_list', $merged_allow_list );
|
||||
|
||||
// Delete the old option if the update was successful.
|
||||
// Check the values directly as `update_raw_option()` returns false if the value hasn't changed.
|
||||
if ( Jetpack_Options::get_raw_option( 'jetpack_waf_ip_allow_list' ) === $merged_allow_list ) {
|
||||
delete_option( 'jetpack_protect_whitelist' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME's option value.
|
||||
* Merges the deprecated IP allow list from the brute force protection module
|
||||
* with the existing option value, and flags that the WAF needs to be updated.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @param array $waf_allow_list The current value of the option.
|
||||
*
|
||||
* @return array The merged IP allow list.
|
||||
*/
|
||||
public static function filter_option_waf_ip_allow_list( $waf_allow_list ) {
|
||||
$brute_force_allow_list = Jetpack_Options::get_raw_option( 'jetpack_protect_whitelist', false );
|
||||
if ( false !== $brute_force_allow_list ) {
|
||||
$waf_allow_list = self::merge_ip_allow_lists( $waf_allow_list, $brute_force_allow_list );
|
||||
update_option( Waf_Initializer::NEEDS_UPDATE_OPTION_NAME, true );
|
||||
}
|
||||
|
||||
return $waf_allow_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default option for when the Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME option is not set.
|
||||
*
|
||||
* @param mixed $default The default value to return if the option does not exist in the database.
|
||||
* @param string $option Option name.
|
||||
* @param bool $passed_default Was get_option() passed a default value.
|
||||
*
|
||||
* @return mixed The default value to return if the option does not exist in the database.
|
||||
*/
|
||||
public static function default_option_waf_ip_allow_list( $default, $option, $passed_default ) {
|
||||
// Allow get_option() to override this default value
|
||||
if ( $passed_default ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$waf_allow_list = '';
|
||||
|
||||
// If the brute force option exists, use that and flag that the WAF needs to be updated.
|
||||
$brute_force_allow_list = Jetpack_Options::get_raw_option( 'jetpack_protect_whitelist', false );
|
||||
if ( false !== $brute_force_allow_list ) {
|
||||
$waf_allow_list = self::merge_ip_allow_lists( $waf_allow_list, $brute_force_allow_list );
|
||||
update_option( Waf_Initializer::NEEDS_UPDATE_OPTION_NAME, true );
|
||||
}
|
||||
|
||||
return $waf_allow_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the brute force protection code is being run by an older version of Jetpack (< 12.0).
|
||||
*
|
||||
* @since 0.11.1
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_brute_force_running_in_jetpack() {
|
||||
return defined( 'JETPACK__VERSION' ) && version_compare( JETPACK__VERSION, '12', '<' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default the allow list enabled option to the value of the generic IP lists enabled option it replaced.
|
||||
*
|
||||
* @since 0.17.0
|
||||
*
|
||||
* @param mixed $default The default value to return if the option does not exist in the database.
|
||||
* @param string $option Option name.
|
||||
* @param bool $passed_default Was get_option() passed a default value.
|
||||
*
|
||||
* @return mixed The default value to return if the option does not exist in the database.
|
||||
*/
|
||||
public static function default_option_waf_ip_allow_list_enabled( $default, $option, $passed_default ) {
|
||||
// Allow get_option() to override this default value
|
||||
if ( $passed_default ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// If the deprecated IP lists option was set to false, disable the allow list.
|
||||
// @phan-suppress-next-line PhanDeprecatedClassConstant -- Needed for backwards compatibility.
|
||||
$deprecated_option = Jetpack_Options::get_raw_option( Waf_Rules_Manager::IP_LISTS_ENABLED_OPTION_NAME, true );
|
||||
if ( ! $deprecated_option ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the allow list is empty, disable the allow list.
|
||||
if ( ! Jetpack_Options::get_raw_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default to enabling the allow list.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default the block list enabled option to the value of the generic IP lists enabled option it replaced.
|
||||
*
|
||||
* @since 0.17.0
|
||||
*
|
||||
* @param mixed $default The default value to return if the option does not exist in the database.
|
||||
* @param string $option Option name.
|
||||
* @param bool $passed_default Was get_option() passed a default value.
|
||||
*
|
||||
* @return mixed The default value to return if the option does not exist in the database.
|
||||
*/
|
||||
public static function default_option_waf_ip_block_list_enabled( $default, $option, $passed_default ) {
|
||||
// Allow get_option() to override this default value
|
||||
if ( $passed_default ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// @phan-suppress-next-line PhanDeprecatedClassConstant -- Needed for backwards compatibility.
|
||||
return Jetpack_Options::get_raw_option( Waf_Rules_Manager::IP_LISTS_ENABLED_OPTION_NAME, false );
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* Class use to register REST API endpoints used by the WAF
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\Connection\REST_Connector;
|
||||
use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* Defines our endponts.
|
||||
*/
|
||||
class REST_Controller {
|
||||
/**
|
||||
* Register REST API endpoints.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
// Ensure routes are only initialized once.
|
||||
static $routes_registered = false;
|
||||
if ( $routes_registered ) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/waf',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::waf',
|
||||
'permission_callback' => __CLASS__ . '::waf_permissions_callback',
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/waf',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::update_waf',
|
||||
'permission_callback' => __CLASS__ . '::waf_permissions_callback',
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/waf/update-rules',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::update_rules',
|
||||
'permission_callback' => __CLASS__ . '::waf_permissions_callback',
|
||||
)
|
||||
);
|
||||
|
||||
$routes_registered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rules endpoint
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function update_rules() {
|
||||
try {
|
||||
Waf_Rules_Manager::generate_automatic_rules();
|
||||
Waf_Rules_Manager::generate_rules();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => true,
|
||||
'message' => __( 'Rules updated successfully', 'jetpack-waf' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* WAF Endpoint
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function waf() {
|
||||
return rest_ensure_response(
|
||||
array_merge(
|
||||
Waf_Runner::get_config(),
|
||||
array(
|
||||
'waf_supported' => Waf_Runner::is_supported_environment(),
|
||||
'automatic_rules_last_updated' => Waf_Stats::get_automatic_rules_last_updated(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update WAF Endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The API request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function update_waf( $request ) {
|
||||
// Automatic Rules Enabled
|
||||
if ( isset( $request[ Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME ] ) ) {
|
||||
update_option( Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME, $request->get_param( Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME ) ? '1' : '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* IP Lists Enabled
|
||||
*
|
||||
* @deprecated 0.17.0 This is a legacy option maintained here for backwards compatibility.
|
||||
*/
|
||||
if ( isset( $request['jetpack_waf_ip_list'] ) ) {
|
||||
update_option( Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME, $request['jetpack_waf_ip_list'] ? '1' : '' );
|
||||
update_option( Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME, $request['jetpack_waf_ip_list'] ? '1' : '' );
|
||||
}
|
||||
|
||||
// IP Block List
|
||||
if ( isset( $request[ Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME ] ) ) {
|
||||
update_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME, $request[ Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME ] );
|
||||
}
|
||||
if ( isset( $request[ Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME ] ) ) {
|
||||
update_option( Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME, $request[ Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME ] ? '1' : '' );
|
||||
}
|
||||
|
||||
// IP Allow List
|
||||
if ( isset( $request[ Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ] ) ) {
|
||||
update_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME, $request[ Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ] );
|
||||
}
|
||||
if ( isset( $request[ Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME ] ) ) {
|
||||
update_option( Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME, $request[ Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME ] ? '1' : '' );
|
||||
}
|
||||
|
||||
// Share Data
|
||||
if ( isset( $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ) ) {
|
||||
// If a user disabled the regular share we should disable the debug share data option.
|
||||
if ( ! $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ) {
|
||||
update_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, '' );
|
||||
}
|
||||
|
||||
update_option( Waf_Runner::SHARE_DATA_OPTION_NAME, $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ? '1' : '' );
|
||||
}
|
||||
|
||||
// Share Debug Data
|
||||
if ( isset( $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] ) ) {
|
||||
// If a user toggles the debug share we should enable the regular share data option.
|
||||
if ( $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] ) {
|
||||
update_option( Waf_Runner::SHARE_DATA_OPTION_NAME, 1 );
|
||||
}
|
||||
|
||||
update_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] ? '1' : '' );
|
||||
}
|
||||
|
||||
// Brute Force Protection
|
||||
if ( isset( $request['brute_force_protection'] ) ) {
|
||||
$enable_brute_force = (bool) $request['brute_force_protection'];
|
||||
$brute_force_protection_toggled =
|
||||
$enable_brute_force
|
||||
? Brute_Force_Protection::enable()
|
||||
: Brute_Force_Protection::disable();
|
||||
|
||||
if ( ! $brute_force_protection_toggled ) {
|
||||
return new WP_Error(
|
||||
$enable_brute_force
|
||||
? 'brute_force_protection_activation_failed'
|
||||
: 'brute_force_protection_deactivation_failed',
|
||||
$enable_brute_force
|
||||
? __( 'Brute force protection could not be activated.', 'jetpack-waf' )
|
||||
: __( 'Brute force protection could not be deactivated.', 'jetpack-waf' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only attempt to update the WAF if the module is supported
|
||||
if ( Waf_Runner::is_supported_environment() ) {
|
||||
try {
|
||||
Waf_Runner::update_waf();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
}
|
||||
|
||||
return self::waf();
|
||||
}
|
||||
|
||||
/**
|
||||
* WAF Endpoint Permissions Callback
|
||||
*
|
||||
* @return bool|WP_Error True if user can view the Jetpack admin page.
|
||||
*/
|
||||
public static function waf_permissions_callback() {
|
||||
if ( current_user_can( 'manage_options' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'invalid_user_permission_manage_options',
|
||||
REST_Connector::get_user_permissions_error_msg(),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Class used to define WAF Blocked Login Page.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\Redirect;
|
||||
|
||||
/**
|
||||
* WAF Blocked Login Page class.
|
||||
*/
|
||||
class Waf_Blocked_Login_Page extends Blocked_Login_Page {
|
||||
|
||||
/**
|
||||
* Instance of the class.
|
||||
*
|
||||
* @var Waf_Blocked_Login_Page
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Instance of the class.
|
||||
*
|
||||
* @param string $ip_address IP address.
|
||||
*
|
||||
* @return Waf_Blocked_Login_Page
|
||||
*/
|
||||
public static function instance( $ip_address ) {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self( $ip_address );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the help URL for the WAF.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_help_url() {
|
||||
return Redirect::get_url( 'jetpack-support-protect-troubleshooting-protect' );
|
||||
}
|
||||
}
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
/**
|
||||
* Blocklog manager for the WAF
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
/**
|
||||
* Class used to manage blocklog operations
|
||||
*/
|
||||
class Waf_Blocklog_Manager {
|
||||
|
||||
const BLOCKLOG_OPTION_NAME_DAILY_SUMMARY = 'jetpack_waf_blocklog_daily_summary';
|
||||
const BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT = 'jetpack_waf_all_time_block_count';
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*
|
||||
* @var \mysqli|null
|
||||
*/
|
||||
private static $db_connection = null;
|
||||
|
||||
/**
|
||||
* Gets the path to the waf-blocklog file.
|
||||
*
|
||||
* @return string The waf-blocklog file path.
|
||||
*/
|
||||
public static function get_blocklog_file_path() {
|
||||
return trailingslashit( JETPACK_WAF_DIR ) . 'waf-blocklog';
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to WordPress database.
|
||||
*
|
||||
* @return \mysqli|null
|
||||
*/
|
||||
private static function connect_to_wordpress_db() {
|
||||
if ( self::$db_connection !== null ) {
|
||||
return self::$db_connection;
|
||||
}
|
||||
|
||||
if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
require_once JETPACK_WAF_WPCONFIG;
|
||||
// @phan-suppress-next-line PhanUndeclaredConstant - These constants are defined in the wp-config file.
|
||||
$conn = new \mysqli( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli
|
||||
|
||||
if ( $conn->connect_error ) {
|
||||
error_log( 'Could not connect to the database:' . $conn->connect_error );
|
||||
return null;
|
||||
}
|
||||
|
||||
self::$db_connection = $conn;
|
||||
return self::$db_connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function close_db_connection() {
|
||||
if ( self::$db_connection ) {
|
||||
self::$db_connection->close();
|
||||
self::$db_connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a value for storage in a WordPress option.
|
||||
*
|
||||
* @param mixed $value The value to serialize.
|
||||
* @return string The serialized value.
|
||||
*/
|
||||
private static function serialize_option_value( $value ) {
|
||||
return serialize( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserialize a value from a WordPress option.
|
||||
*
|
||||
* @param string $value The serialized value.
|
||||
* @return mixed The unserialized value.
|
||||
*/
|
||||
private static function unserialize_option_value( string $value ) {
|
||||
return unserialize( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the log table when plugin is activated.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function create_blocklog_table() {
|
||||
global $wpdb;
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
$sql = "
|
||||
CREATE TABLE {$wpdb->prefix}jetpack_waf_blocklog (
|
||||
log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
timestamp datetime NOT NULL,
|
||||
rule_id BIGINT NOT NULL,
|
||||
reason longtext NOT NULL,
|
||||
PRIMARY KEY (log_id),
|
||||
KEY timestamp (timestamp)
|
||||
)
|
||||
";
|
||||
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Write block logs to database.
|
||||
*
|
||||
* @param array $log_data Log data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function write_blocklog_row( $log_data ) {
|
||||
$conn = self::connect_to_wordpress_db();
|
||||
|
||||
if ( ! $conn ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $table_prefix;
|
||||
|
||||
$statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" );
|
||||
if ( false !== $statement ) {
|
||||
$statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] );
|
||||
$statement->execute();
|
||||
|
||||
if ( $conn->insert_id > 100 ) {
|
||||
$conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the daily summary stats from the database.
|
||||
*
|
||||
* @return array The daily summary stats.
|
||||
*/
|
||||
private static function get_daily_summary() {
|
||||
global $table_prefix;
|
||||
$db_connection = self::connect_to_wordpress_db();
|
||||
if ( ! $db_connection ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY . "'" );
|
||||
if ( ! $result ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
if ( ! $row ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$daily_summary = self::unserialize_option_value( $row['option_value'] );
|
||||
$result->free();
|
||||
|
||||
return is_array( $daily_summary ) ? $daily_summary : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the current date's daily summary stat.
|
||||
*
|
||||
* @param array $current_value The current value of the daily summary.
|
||||
*
|
||||
* @return array The updated daily summary.
|
||||
*/
|
||||
public static function increment_daily_summary( array $current_value ) {
|
||||
$date = gmdate( 'Y-m-d' );
|
||||
$value = intval( $current_value[ $date ] ?? 0 );
|
||||
$current_value[ $date ] = $value + 1;
|
||||
|
||||
return $current_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the daily summary option in the database.
|
||||
*
|
||||
* @param array $value The value to update.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function write_daily_summary_row( array $value ) {
|
||||
global $table_prefix;
|
||||
$option_name = self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY;
|
||||
|
||||
$db_connection = self::connect_to_wordpress_db();
|
||||
if ( ! $db_connection ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$updated_value = self::serialize_option_value( $value );
|
||||
|
||||
$statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
|
||||
if ( false !== $statement ) {
|
||||
$statement->bind_param( 'sss', $option_name, $updated_value, $updated_value );
|
||||
$statement->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the daily summary stats for the current date.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function write_daily_summary() {
|
||||
$stats = self::get_daily_summary();
|
||||
$stats = self::increment_daily_summary( $stats );
|
||||
$stats = self::filter_last_30_days( $stats );
|
||||
|
||||
self::write_daily_summary_row( $stats );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the all-time block count value from the database.
|
||||
*
|
||||
* @return int The all-time block count.
|
||||
*/
|
||||
private static function get_all_time_block_count_value() {
|
||||
global $table_prefix;
|
||||
$db_connection = self::connect_to_wordpress_db();
|
||||
if ( ! $db_connection ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT . "'" );
|
||||
if ( ! $result ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
if ( ! $row ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$all_time_block_count = intval( $row['option_value'] );
|
||||
$result->free();
|
||||
|
||||
return $all_time_block_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the all-time block count value in the database.
|
||||
*
|
||||
* @param int $value The value to update.
|
||||
* @return void
|
||||
*/
|
||||
private static function write_all_time_block_count_row( int $value ) {
|
||||
global $table_prefix;
|
||||
$option_name = self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT;
|
||||
|
||||
$db_connection = self::connect_to_wordpress_db();
|
||||
if ( ! $db_connection ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
|
||||
if ( false !== $statement ) {
|
||||
$statement->bind_param( 'sii', $option_name, $value, $value );
|
||||
$statement->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the all-time stats.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function write_all_time_block_count() {
|
||||
$block_count = self::get_all_time_block_count_value();
|
||||
if ( ! $block_count ) {
|
||||
$block_count = self::get_default_all_time_stat_value();
|
||||
}
|
||||
|
||||
self::write_all_time_block_count_row( $block_count + 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the stats to retain only data for the last 30 days.
|
||||
*
|
||||
* @param array $stats The array of stats to prune.
|
||||
*
|
||||
* @return array Pruned stats array.
|
||||
*/
|
||||
public static function filter_last_30_days( array $stats ) {
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
$one_month_ago = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
|
||||
|
||||
return array_filter(
|
||||
$stats,
|
||||
function ( $date ) use ( $one_month_ago, $today ) {
|
||||
return $date >= $one_month_ago && $date <= $today;
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of blocked requests for today.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_current_day_block_count() {
|
||||
$stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
return $stats[ $today ] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of blocked requests for last thirty days.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_thirty_days_block_counts() {
|
||||
$stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
|
||||
$total_blocks = 0;
|
||||
|
||||
foreach ( $stats as $count ) {
|
||||
$total_blocks += intval( $count );
|
||||
}
|
||||
|
||||
return $total_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of blocked requests for all time.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_all_time_block_count() {
|
||||
$all_time_block_count = get_option( self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT, false );
|
||||
|
||||
if ( false !== $all_time_block_count ) {
|
||||
return intval( $all_time_block_count );
|
||||
}
|
||||
|
||||
return self::get_default_all_time_stat_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the initial all-time stats value.
|
||||
*
|
||||
* @return int The initial all-time stats value.
|
||||
*/
|
||||
private static function get_default_all_time_stat_value() {
|
||||
$conn = self::connect_to_wordpress_db();
|
||||
if ( ! $conn ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
global $table_prefix;
|
||||
|
||||
$last_log_id_result = $conn->query( "SELECT log_id FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id DESC LIMIT 1" );
|
||||
|
||||
$all_time_block_count = 0;
|
||||
|
||||
if ( $last_log_id_result && $last_log_id_result->num_rows > 0 ) {
|
||||
$row = $last_log_id_result->fetch_assoc();
|
||||
if ( $row !== null && isset( $row['log_id'] ) ) {
|
||||
$all_time_block_count = $row['log_id'];
|
||||
}
|
||||
}
|
||||
|
||||
return intval( $all_time_block_count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the headers for logging purposes.
|
||||
*
|
||||
* @return array The headers.
|
||||
*/
|
||||
public static function get_request_headers() {
|
||||
$all_headers = getallheaders();
|
||||
$exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
|
||||
|
||||
foreach ( $exclude_headers as $header ) {
|
||||
unset( $all_headers[ $header ] );
|
||||
}
|
||||
|
||||
return $all_headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write block logs. We won't write to the file if it exceeds 100 mb.
|
||||
*
|
||||
* @param string $rule_id The rule ID that triggered the block.
|
||||
* @param string $reason The reason for the block.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function write_blocklog( $rule_id, $reason ) {
|
||||
$log_data = array();
|
||||
$log_data['rule_id'] = $rule_id;
|
||||
$log_data['reason'] = $reason;
|
||||
$log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
|
||||
$log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['get_params'] = json_encode( $_GET, JSON_UNESCAPED_SLASHES );
|
||||
|
||||
if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
|
||||
$log_data['post_params'] = json_encode( $_POST, JSON_UNESCAPED_SLASHES );
|
||||
$log_data['headers'] = self::get_request_headers();
|
||||
}
|
||||
|
||||
if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
|
||||
$file_path = JETPACK_WAF_DIR . '/waf-blocklog';
|
||||
$file_exists = file_exists( $file_path );
|
||||
|
||||
if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) {
|
||||
$fp = fopen( $file_path, 'a+' );
|
||||
|
||||
if ( $fp ) {
|
||||
try {
|
||||
fwrite( $fp, json_encode( $log_data, JSON_UNESCAPED_SLASHES ) . "\n" );
|
||||
} finally {
|
||||
fclose( $fp );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self::write_daily_summary();
|
||||
self::write_all_time_block_count();
|
||||
self::write_blocklog_row( $log_data );
|
||||
self::close_db_connection();
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/**
|
||||
* CLI handler for Jetpack Waf.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use WP_CLI;
|
||||
use WP_CLI_Command;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the WAF, change its mode, or generate its rules.
|
||||
*/
|
||||
class CLI extends WP_CLI_Command {
|
||||
/**
|
||||
* View or set the current mode of the WAF.
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [<mode>]
|
||||
* : The new mode to be set.
|
||||
* ---
|
||||
* options:
|
||||
* - silent
|
||||
* - normal
|
||||
* ---
|
||||
*
|
||||
* @param array $args Arguments passed to CLI.
|
||||
* @return void|null
|
||||
* @throws WP_CLI\ExitException If there is an error switching the mode.
|
||||
*/
|
||||
public function mode( $args ) {
|
||||
if ( count( $args ) > 1 ) {
|
||||
|
||||
return WP_CLI::error( __( 'Only one mode may be specified.', 'jetpack-waf' ) );
|
||||
}
|
||||
if ( count( $args ) === 1 ) {
|
||||
if ( ! Waf_Runner::is_allowed_mode( $args[0] ) ) {
|
||||
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
/* translators: %1$s is the mode that was actually found. Also note that the expected "silent" and "normal" are hard-coded strings and must therefore stay the same in any translation. */
|
||||
__( 'Invalid mode: %1$s. Expected "silent" or "normal".', 'jetpack-waf' ),
|
||||
$args[0]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
update_option( Waf_Runner::MODE_OPTION_NAME, $args[0] );
|
||||
|
||||
try {
|
||||
( new Waf_Standalone_Bootstrap() )->generate();
|
||||
} catch ( \Exception $e ) {
|
||||
WP_CLI::warning(
|
||||
sprintf(
|
||||
/* translators: %1$s is the unexpected error message. */
|
||||
__( 'Unable to generate waf bootstrap - standalone mode may not work properly: %1$s', 'jetpack-waf' ),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return WP_CLI::success(
|
||||
sprintf(
|
||||
/* translators: %1$s is the name of the mode that was just switched to. */
|
||||
__( 'Jetpack WAF mode switched to "%1$s".', 'jetpack-waf' ),
|
||||
get_option( Waf_Runner::MODE_OPTION_NAME )
|
||||
)
|
||||
);
|
||||
}
|
||||
WP_CLI::line(
|
||||
sprintf(
|
||||
/* translators: %1$s is the name of the mode that the waf is currently running in. */
|
||||
__( 'Jetpack WAF is running in "%1$s" mode.', 'jetpack-waf' ),
|
||||
get_option( Waf_Runner::MODE_OPTION_NAME )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the WAF to run.
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [<mode>]
|
||||
* : The new mode to be set.
|
||||
* ---
|
||||
* options:
|
||||
* - silent
|
||||
* - normal
|
||||
* ---
|
||||
*
|
||||
* @param array $args Arguments passed to CLI.
|
||||
* @return void|null
|
||||
* @throws WP_CLI\ExitException If there is an error switching the mode.
|
||||
*/
|
||||
public function setup( $args ) {
|
||||
// Let is_allowed_mode know we are running from the CLI
|
||||
define( 'WAF_CLI_MODE', $args[0] );
|
||||
|
||||
// Set the mode and generate the bootstrap
|
||||
$this->mode( array( $args[0] ) );
|
||||
|
||||
try {
|
||||
// Add relevant options and generate the rules.php file
|
||||
Waf_Runner::activate();
|
||||
} catch ( \Exception $e ) {
|
||||
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
/* translators: %1$s is the unexpected error message. */
|
||||
__( 'Jetpack WAF rules file failed to generate: %1$s', 'jetpack-waf' ),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return WP_CLI::success( __( 'Jetpack WAF has successfully been set up.', 'jetpack-waf' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the WAF options.
|
||||
*
|
||||
* @return void|null
|
||||
* @throws WP_CLI\ExitException If deactivating has failures.
|
||||
*/
|
||||
public function teardown() {
|
||||
try {
|
||||
Waf_Runner::deactivate();
|
||||
} catch ( \Exception $e ) {
|
||||
WP_CLI::error( __( 'Jetpack WAF failed to fully deactivate.', 'jetpack-waf' ) );
|
||||
}
|
||||
|
||||
return WP_CLI::success( __( 'Jetpack WAF has been deactivated.', 'jetpack-waf' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the rules.php file with latest rules for the WAF.
|
||||
*
|
||||
* @return void|null
|
||||
* @throws WP_CLI\ExitException If there is an error switching the mode.
|
||||
*/
|
||||
public function generate_rules() {
|
||||
try {
|
||||
Waf_Constants::define_entrypoint();
|
||||
Waf_Rules_Manager::generate_automatic_rules();
|
||||
Waf_Rules_Manager::generate_rules();
|
||||
} catch ( \Exception $e ) {
|
||||
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
/* translators: %1$s is the unexpected error message. */
|
||||
__( 'Jetpack WAF rules file failed to generate: %1$s', 'jetpack-waf' ),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return WP_CLI::success(
|
||||
sprintf(
|
||||
/* translators: %1$s is the name of the mode that was just switched to. */
|
||||
__( 'Jetpack WAF rules successfully created to: "%1$s".', 'jetpack-waf' ),
|
||||
Waf_Runner::get_waf_file_path( JETPACK_WAF_ENTRYPOINT )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
/**
|
||||
* Class use to define the constants used by the WAF
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Defines our constants.
|
||||
*/
|
||||
class Waf_Constants {
|
||||
/**
|
||||
* Initializes the constants required for generating the bootstrap, if they have not been initialized yet.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function initialize_bootstrap_constants() {
|
||||
self::define_waf_directory();
|
||||
self::define_wpconfig_path();
|
||||
self::define_killswitch();
|
||||
self::define_entrypoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatiblity patch for cases where an outdated Waf_Constants class has been autoloaded by
|
||||
* the standalone bootstrap execution at the beginning of the current request.
|
||||
*/
|
||||
public static function initialize_constants() {
|
||||
self::initialize_bootstrap_constants();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path to the WAF directory if it has not been set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function define_waf_directory() {
|
||||
if ( ! defined( 'JETPACK_WAF_DIR' ) ) {
|
||||
define( 'JETPACK_WAF_DIR', trailingslashit( WP_CONTENT_DIR ) . 'jetpack-waf' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path to the wp-config.php file if it has not been set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function define_wpconfig_path() {
|
||||
if ( ! defined( 'JETPACK_WAF_WPCONFIG' ) ) {
|
||||
define( 'JETPACK_WAF_WPCONFIG', trailingslashit( WP_CONTENT_DIR ) . '../wp-config.php' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the killswitch definition if it has not been set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function define_killswitch() {
|
||||
if ( ! defined( 'DISABLE_JETPACK_WAF' ) ) {
|
||||
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
|
||||
$is_atomic = ( new Host() )->is_atomic_platform();
|
||||
$is_atomic_on_jn = defined( 'IS_ATOMIC_JN' ) ?? IS_ATOMIC_JN;
|
||||
define( 'DISABLE_JETPACK_WAF', $is_wpcom || ( $is_atomic && ! $is_atomic_on_jn ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mode definition if it has not been set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function define_mode() {
|
||||
if ( ! defined( 'JETPACK_WAF_MODE' ) ) {
|
||||
$mode_option = get_option( Waf_Runner::MODE_OPTION_NAME );
|
||||
define( 'JETPACK_WAF_MODE', $mode_option );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the entrypoint definition if it has not been set.
|
||||
*/
|
||||
public static function define_entrypoint() {
|
||||
if ( ! defined( 'JETPACK_WAF_ENTRYPOINT' ) ) {
|
||||
define( 'JETPACK_WAF_ENTRYPOINT', 'rules/rules.php' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the share data definition if it has not been set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function define_share_data() {
|
||||
if ( ! defined( 'JETPACK_WAF_SHARE_DATA' ) ) {
|
||||
$share_data_option = false;
|
||||
if ( function_exists( 'get_option' ) ) {
|
||||
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
define( 'JETPACK_WAF_SHARE_DATA', $share_data_option );
|
||||
}
|
||||
if ( ! defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) ) {
|
||||
$share_debug_data_option = false;
|
||||
if ( function_exists( 'get_option' ) ) {
|
||||
$share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
define( 'JETPACK_WAF_SHARE_DEBUG_DATA', $share_debug_data_option );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the brute force protection's API host definition if it has not been set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function define_brute_force_api_host() {
|
||||
if ( ! defined( 'JETPACK_PROTECT__API_HOST' ) ) {
|
||||
define( 'JETPACK_PROTECT__API_HOST', 'https://api.bruteprotect.com/' );
|
||||
}
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
/**
|
||||
* Class use to initialize the WAF module.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
|
||||
use WP_Error;
|
||||
use WP_Upgrader;
|
||||
|
||||
/**
|
||||
* Initializes the module
|
||||
*/
|
||||
class Waf_Initializer {
|
||||
|
||||
/**
|
||||
* Option for storing whether the WAF files are potentially out of date.
|
||||
*
|
||||
* @var string NEEDS_UPDATE_OPTION_NAME
|
||||
*/
|
||||
const NEEDS_UPDATE_OPTION_NAME = 'jetpack_waf_needs_update';
|
||||
|
||||
/**
|
||||
* Initializes the configurations needed for the waf module.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
// Do not run in unsupported environments
|
||||
add_action( 'jetpack_get_available_modules', __CLASS__ . '::remove_module_on_unsupported_environments' );
|
||||
add_action( 'jetpack_get_available_standalone_modules', __CLASS__ . '::remove_standalone_module_on_unsupported_environments' );
|
||||
|
||||
// Ensure backwards compatibility
|
||||
Waf_Compatibility::add_compatibility_hooks();
|
||||
|
||||
// Register REST routes. Use a static callable so the controller class is not
|
||||
// loaded into memory/opcache on requests that never reach `rest_api_init`.
|
||||
add_action( 'rest_api_init', array( REST_Controller::class, 'register_rest_routes' ) );
|
||||
|
||||
// Update the WAF after installing or upgrading a relevant Jetpack plugin
|
||||
add_action( 'upgrader_process_complete', __CLASS__ . '::update_waf_after_plugin_upgrade', 10, 2 );
|
||||
|
||||
// Check for compatibility updates
|
||||
add_action( 'admin_init', __CLASS__ . '::check_for_updates' );
|
||||
|
||||
// WAF activation/deactivation hooks
|
||||
add_action( 'jetpack_activate_module_waf', __CLASS__ . '::on_waf_activation' );
|
||||
add_action( 'jetpack_deactivate_module_waf', __CLASS__ . '::on_waf_deactivation' );
|
||||
|
||||
// Brute force protection activation/deactivation hooks
|
||||
add_action( 'jetpack_activate_module_protect', __CLASS__ . '::on_brute_force_protection_activation' );
|
||||
add_action( 'jetpack_deactivate_module_protect', __CLASS__ . '::on_brute_force_protection_deactivation' );
|
||||
|
||||
// Run brute force protection
|
||||
Brute_Force_Protection::initialize();
|
||||
|
||||
// Run the WAF
|
||||
if ( Waf_Runner::is_supported_environment() ) {
|
||||
Waf_Runner::initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the WAF on module activation.
|
||||
*
|
||||
* @return bool|WP_Error True if the WAF activation is successful, WP_Error otherwise.
|
||||
*/
|
||||
public static function on_waf_activation() {
|
||||
update_option( Waf_Runner::MODE_OPTION_NAME, 'normal' );
|
||||
add_option( Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME, false );
|
||||
|
||||
try {
|
||||
Waf_Runner::activate();
|
||||
( new Waf_Standalone_Bootstrap() )->generate();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the Brute force protection on module activation.
|
||||
*
|
||||
* @return bool True if the Brute force protection activation is successful
|
||||
*/
|
||||
public static function on_brute_force_protection_activation() {
|
||||
$brute_force_protection = Brute_Force_Protection::instance();
|
||||
$brute_force_protection->on_activation();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the WAF on module deactivation.
|
||||
*
|
||||
* @return bool|WP_Error True if the WAF deactivation is successful, WP_Error otherwise.
|
||||
*/
|
||||
public static function on_waf_deactivation() {
|
||||
try {
|
||||
Waf_Runner::deactivate();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the Brute force protection on module deactivation.
|
||||
*
|
||||
* @return bool True if the Brute force protection deactivation is successful.
|
||||
*/
|
||||
public static function on_brute_force_protection_deactivation() {
|
||||
$brute_force_protection = Brute_Force_Protection::instance();
|
||||
$brute_force_protection->on_deactivation();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the WAF after upgrader process is complete.
|
||||
*
|
||||
* @param WP_Upgrader $upgrader WP_Upgrader instance. In other contexts this might be a Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance.
|
||||
* @param array $hook_extra Array of bulk item update data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function update_waf_after_plugin_upgrade( $upgrader, $hook_extra ) {
|
||||
$jetpack_text_domains_with_waf = array( 'jetpack', 'jetpack-protect' );
|
||||
$jetpack_plugins_with_waf = array( 'jetpack/jetpack.php', 'jetpack-protect/jetpack-protect.php' );
|
||||
|
||||
$hook_extra['type'] = $hook_extra['type'] ?? null;
|
||||
$hook_extra['action'] = $hook_extra['action'] ?? null;
|
||||
$hook_extra['plugins'] = $hook_extra['plugins'] ?? array();
|
||||
|
||||
// Only run on upgrades affecting plugins
|
||||
if ( 'plugin' !== $hook_extra['type'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only run on updates and installations
|
||||
if ( 'update' !== $hook_extra['action'] && 'install' !== $hook_extra['action'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only run when Jetpack plugins were affected
|
||||
if ( 'update' === $hook_extra['action'] &&
|
||||
! empty( $hook_extra['plugins'] ) &&
|
||||
empty( array_intersect( $jetpack_plugins_with_waf, $hook_extra['plugins'] ) )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if ( 'install' === $hook_extra['action'] &&
|
||||
! empty( $upgrader->new_plugin_data['TextDomain'] ) &&
|
||||
empty( in_array( $upgrader->new_plugin_data['TextDomain'] ?? null, $jetpack_text_domains_with_waf, true ) )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( self::NEEDS_UPDATE_OPTION_NAME, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for WAF update
|
||||
*
|
||||
* Updates the WAF when the "needs update" option is enabled.
|
||||
*
|
||||
* @return bool|WP_Error True if the WAF is up-to-date or was sucessfully updated, WP_Error if the update failed.
|
||||
*/
|
||||
public static function check_for_updates() {
|
||||
if ( get_option( self::NEEDS_UPDATE_OPTION_NAME ) ) {
|
||||
if ( Waf_Runner::is_supported_environment() ) {
|
||||
// Compatiblity patch for cases where an outdated WAF_Constants class has been
|
||||
// autoloaded by the standalone bootstrap execution at the beginning of the current request.
|
||||
if ( ! method_exists( Waf_Constants::class, 'define_mode' ) ) {
|
||||
try {
|
||||
( new Waf_Standalone_Bootstrap() )->generate();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
}
|
||||
|
||||
Waf_Compatibility::run_compatibility_migrations();
|
||||
|
||||
try {
|
||||
Waf_Rules_Manager::generate_ip_rules();
|
||||
Waf_Rules_Manager::generate_rules();
|
||||
( new Waf_Standalone_Bootstrap() )->generate();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
} else {
|
||||
// If the site doesn't support the request firewall,
|
||||
// just migrate the IP allow list used by brute force protection.
|
||||
Waf_Compatibility::migrate_brute_force_protection_ip_allow_list();
|
||||
}
|
||||
|
||||
update_option( self::NEEDS_UPDATE_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the WAF module when on an unsupported platform in Jetpack.
|
||||
*
|
||||
* @param array $modules Filterable value for `jetpack_get_available_modules`.
|
||||
*
|
||||
* @return array Array of module slugs.
|
||||
*/
|
||||
public static function remove_module_on_unsupported_environments( $modules ) {
|
||||
if ( ! Waf_Runner::is_supported_environment() ) {
|
||||
// WAF should never be available on unsupported platforms.
|
||||
unset( $modules['waf'] );
|
||||
}
|
||||
|
||||
return $modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the WAF module when on an unsupported platform in a standalone plugin.
|
||||
*
|
||||
* @param array $modules Filterable value for `jetpack_get_available_standalone_modules`.
|
||||
*
|
||||
* @return array Array of module slugs.
|
||||
*/
|
||||
public static function remove_standalone_module_on_unsupported_environments( $modules ) {
|
||||
if ( ! Waf_Runner::is_supported_environment() ) {
|
||||
// WAF should never be available on unsupported platforms.
|
||||
$modules = array_filter(
|
||||
$modules,
|
||||
function ( $module ) {
|
||||
return $module !== 'waf';
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return $modules;
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
/**
|
||||
* Rule compiler for Jetpack Waf.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
/**
|
||||
* Waf_Operators class
|
||||
*/
|
||||
class Waf_Operators {
|
||||
/**
|
||||
* Returns true if the test string is found at the beginning of the input.
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @param string $test Test.
|
||||
* @return string|false
|
||||
*/
|
||||
public function begins_with( $input, $test ) {
|
||||
if ( '' === $input && '' === $test ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr( $input, 0, strlen( $test ) ) === $test
|
||||
? $test
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the test string is found anywhere in the input.
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @param string $test Test.
|
||||
* @return string|false
|
||||
*/
|
||||
public function contains( $input, $test ) {
|
||||
if ( empty( $input ) || empty( $test ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strpos( $input, $test ) !== false
|
||||
? $test
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the test string with word boundaries is found anywhere in the input.
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @param string $test Test.
|
||||
* @return string|false
|
||||
*/
|
||||
public function contains_word( $input, $test ) {
|
||||
return ( $input === $test || 1 === preg_match( '/\b' . preg_quote( $test, '/' ) . '\b/Ds', $input ) )
|
||||
? $test
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the test string is found at the end of the input.
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @param string $test Test.
|
||||
* @return string|false
|
||||
*/
|
||||
public function ends_with( $input, $test ) {
|
||||
return ( '' === $test || substr( $input, -1 * strlen( $test ) ) === $test )
|
||||
? $test
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the input value is equal to the test value.
|
||||
* If either value cannot be converted to an int it will be treated as 0.
|
||||
*
|
||||
* @param mixed $input Input.
|
||||
* @param mixed $test Test.
|
||||
* @return int|false
|
||||
*/
|
||||
public function eq( $input, $test ) {
|
||||
return intval( $input ) === intval( $test )
|
||||
? $input
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the input value is greater than or equal to the test value.
|
||||
* If either value cannot be converted to an int it will be treated as 0.
|
||||
*
|
||||
* @param mixed $input Input.
|
||||
* @param mixed $test Test.
|
||||
* @return int|false
|
||||
*/
|
||||
public function ge( $input, $test ) {
|
||||
return intval( $input ) >= intval( $test )
|
||||
? $input
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the input value is greater than the test value.
|
||||
* If either value cannot be converted to an int it will be treated as 0.
|
||||
*
|
||||
* @param mixed $input Input.
|
||||
* @param mixed $test Test.
|
||||
* @return int|false
|
||||
*/
|
||||
public function gt( $input, $test ) {
|
||||
return intval( $input ) > intval( $test )
|
||||
? $input
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the input value is less than or equal to the test value.
|
||||
* If either value cannot be converted to an int it will be treated as 0.
|
||||
*
|
||||
* @param mixed $input Input.
|
||||
* @param mixed $test Test.
|
||||
* @return int|false
|
||||
*/
|
||||
public function le( $input, $test ) {
|
||||
return intval( $input ) <= intval( $test )
|
||||
? $input
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the input value is less than the test value.
|
||||
* If either value cannot be converted to an int it will be treated as 0.
|
||||
*
|
||||
* @param mixed $input Input.
|
||||
* @param mixed $test Test.
|
||||
* @return int|false
|
||||
*/
|
||||
public function lt( $input, $test ) {
|
||||
return intval( $input ) < intval( $test )
|
||||
? $input
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false.
|
||||
*
|
||||
* @return false
|
||||
*/
|
||||
public function no_match() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a multi-string matching algorithm to search through $input for a number of given $words.
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @param string[] $words \AhoCorasick\MultiStringMatcher $matcher.
|
||||
* @return string[]|false Returns the words that were found in $input, or FALSE if no words were found.
|
||||
*/
|
||||
public function pm( $input, $words ) {
|
||||
$results = $this->get_multi_string_matcher( $words )->searchIn( $input );
|
||||
|
||||
return isset( $results[0] )
|
||||
? array_map(
|
||||
function ( $r ) {
|
||||
return $r[1]; },
|
||||
$results
|
||||
)
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The last-used pattern-matching algorithm.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $last_multi_string_matcher = array( null, null );
|
||||
|
||||
/**
|
||||
* Creates a matcher that uses the Aho-Corasick algorithm to efficiently find a number of words in an input string.
|
||||
* Caches the last-used matcher so that the same word list doesn't have to be compiled multiple times.
|
||||
*
|
||||
* @param string[] $words Words.
|
||||
* @return \AhoCorasick\MultiStringMatcher
|
||||
*/
|
||||
private function get_multi_string_matcher( $words ) {
|
||||
// only create a new matcher entity if we don't have one already for this word list.
|
||||
if ( $this->last_multi_string_matcher[0] !== $words ) {
|
||||
$this->last_multi_string_matcher = array( $words, new \AhoCorasick\MultiStringMatcher( $words ) );
|
||||
}
|
||||
|
||||
return $this->last_multi_string_matcher[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a regular expression match on the input subject using the given pattern.
|
||||
* Returns false if the pattern does not match, or the substring(s) of the input
|
||||
* that were matched by the pattern.
|
||||
*
|
||||
* @param string $subject Subject.
|
||||
* @param string $pattern Pattern.
|
||||
* @return string[]|false
|
||||
*/
|
||||
public function rx( $subject, $pattern ) {
|
||||
$matched = preg_match( $pattern, $subject, $matches );
|
||||
return 1 === $matched
|
||||
? $matches
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given input string matches the test string.
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @param string $test Test.
|
||||
* @return string|false
|
||||
*/
|
||||
public function streq( $input, $test ) {
|
||||
return $input === $test
|
||||
? $test
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true.
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @return bool
|
||||
*/
|
||||
public function unconditional_match( $input ) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the input string only contains characters within the given byte range
|
||||
*
|
||||
* @param string $input Input.
|
||||
* @param array $valid_range Valid range.
|
||||
* @return string
|
||||
*/
|
||||
public function validate_byte_range( $input, $valid_range ) {
|
||||
if ( '' === $input ) {
|
||||
// an empty string is considered "valid".
|
||||
return false;
|
||||
}
|
||||
$i = 0;
|
||||
while ( isset( $input[ $i ] ) ) {
|
||||
$n = ord( $input[ $i ] );
|
||||
if ( $n < $valid_range['min'] || $n > $valid_range['max'] ) {
|
||||
return $input[ $i ];
|
||||
}
|
||||
$valid = false;
|
||||
foreach ( $valid_range['range'] as $b ) {
|
||||
if ( $n === $b || is_array( $b ) && $n >= $b[0] && $n <= $b[1] ) {
|
||||
$valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( ! $valid ) {
|
||||
return $input[ $i ];
|
||||
}
|
||||
++$i;
|
||||
}
|
||||
|
||||
// if there weren't any invalid bytes, return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the input value is found anywhere inside the test value
|
||||
* (i.e. the inverse of @contains)
|
||||
*
|
||||
* @param mixed $input Input.
|
||||
* @param mixed $test Test.
|
||||
* @return string|false
|
||||
*/
|
||||
public function within( $input, $test ) {
|
||||
if ( '' === $input || '' === $test ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strpos( $test, $input ) !== false
|
||||
? $input
|
||||
: false;
|
||||
}
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
/**
|
||||
* HTTP request representation specific for the WAF.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
<<<'PHAN'
|
||||
@phan-type RequestFile = array{ name: string, filename: string }
|
||||
PHAN;
|
||||
|
||||
/**
|
||||
* Request representation.
|
||||
*/
|
||||
class Waf_Request {
|
||||
/**
|
||||
* The request URL, broken into three pieces: the host, the filename, and the query string
|
||||
*
|
||||
* @example for `https://wordpress.com/index.php?myvar=red`
|
||||
* $this->url = [ 'https://wordpress.com', '/index.php', '?myvar=red' ]
|
||||
* @var array{0: string, 1: string, 2: string}|null
|
||||
*/
|
||||
protected $url = null;
|
||||
|
||||
/**
|
||||
* Trusted proxies.
|
||||
*
|
||||
* @var array List of trusted proxy IP addresses.
|
||||
*/
|
||||
private $trusted_proxies = array();
|
||||
|
||||
/**
|
||||
* Trusted headers.
|
||||
*
|
||||
* @var array List of headers to trust from the trusted proxies.
|
||||
*/
|
||||
private $trusted_headers = array();
|
||||
|
||||
/**
|
||||
* Sets the list of IP addresses for the proxies to trust. Trusted headers will only be accepted as the
|
||||
* user IP address from these IP adresses.
|
||||
*
|
||||
* Popular choices include:
|
||||
* - 192.168.0.1
|
||||
* - 10.0.0.1
|
||||
*
|
||||
* @param array $proxies List of proxy IP addresses.
|
||||
* @return void
|
||||
*/
|
||||
public function set_trusted_proxies( $proxies ) {
|
||||
$this->trusted_proxies = (array) $proxies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of headers to be trusted from the proxies. These headers will only be taken into account
|
||||
* if the request comes from a trusted proxy as configured with set_trusted_proxies().
|
||||
*
|
||||
* Popular choices include:
|
||||
* - HTTP_CLIENT_IP
|
||||
* - HTTP_X_FORWARDED_FOR
|
||||
* - HTTP_X_FORWARDED
|
||||
* - HTTP_X_CLUSTER_CLIENT_IP
|
||||
* - HTTP_FORWARDED_FOR
|
||||
* - HTTP_FORWARDED
|
||||
*
|
||||
* @param array $headers List of HTTP header strings.
|
||||
* @return void
|
||||
*/
|
||||
public function set_trusted_headers( $headers ) {
|
||||
$this->trusted_headers = (array) $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the users real IP address based on the settings passed to set_trusted_proxies() and
|
||||
* set_trusted_headers() before. On CLI, this will be null.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_real_user_ip_address() {
|
||||
$remote_addr = ! empty( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
|
||||
if ( in_array( $remote_addr, $this->trusted_proxies, true ) ) {
|
||||
$ip_by_header = $this->get_ip_by_header( array_merge( $this->trusted_headers, array( 'REMOTE_ADDR' ) ) );
|
||||
if ( ! empty( $ip_by_header ) ) {
|
||||
return $ip_by_header;
|
||||
}
|
||||
}
|
||||
|
||||
return $remote_addr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through a given list of HTTP headers and attempts to get the IP address from the header that
|
||||
* a proxy sends along. Make sure you trust the IP address before calling this method.
|
||||
*
|
||||
* @param array $headers The list of headers to check.
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_ip_by_header( $headers ) {
|
||||
foreach ( $headers as $key ) {
|
||||
if ( isset( $_SERVER[ $key ] ) ) {
|
||||
foreach ( explode( ',', wp_unslash( $_SERVER[ $key ] ) ) as $ip ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- filter_var is applied below.
|
||||
$ip = trim( $ip );
|
||||
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the headers that were sent with this request
|
||||
*
|
||||
* @return array{0: string, 1: scalar}[]
|
||||
*/
|
||||
public function get_headers() {
|
||||
$value = array();
|
||||
$has_content_length = false;
|
||||
foreach ( $_SERVER as $k => $v ) {
|
||||
$k = strtolower( $k );
|
||||
if ( 'http_' === substr( $k, 0, 5 ) ) {
|
||||
$value[] = array( $this->normalize_header_name( substr( $k, 5 ) ), $v );
|
||||
} elseif ( 'content_type' === $k && '' !== $v ) {
|
||||
$value[] = array( 'content-type', $v );
|
||||
} elseif ( 'content_length' === $k && '' !== $v ) {
|
||||
$has_content_length = true;
|
||||
$value[] = array( 'content-length', $v );
|
||||
}
|
||||
}
|
||||
if ( ! $has_content_length ) {
|
||||
$value[] = array( 'content-length', '0' );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a specific header that was sent with this request
|
||||
*
|
||||
* @param string $name The name of the header to retrieve.
|
||||
* @return string
|
||||
*/
|
||||
public function get_header( $name ) {
|
||||
$name = $this->normalize_header_name( $name );
|
||||
foreach ( $this->get_headers() as list( $header_name, $header_value ) ) {
|
||||
if ( $header_name === $name ) {
|
||||
return $header_value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a header name to all-lowercase and replace spaces and underscores with dashes.
|
||||
*
|
||||
* @param string $name The header name to normalize.
|
||||
* @return string
|
||||
*/
|
||||
public function normalize_header_name( $name ) {
|
||||
return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method for this request (GET, POST, etc).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_method() {
|
||||
return isset( $_SERVER['REQUEST_METHOD'] )
|
||||
? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ), FILTER_DEFAULT )
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the protocol for this request (HTTP, HTTPS, etc)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_protocol() {
|
||||
return isset( $_SERVER['SERVER_PROTOCOL'] )
|
||||
? filter_var( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ), FILTER_DEFAULT )
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL parts for this request.
|
||||
*
|
||||
* @see $this->url
|
||||
* @return array{0: string, 1: string, 2: string}
|
||||
*/
|
||||
protected function get_url() {
|
||||
if ( null !== $this->url ) {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
$uri = isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_DEFAULT ) : '/';
|
||||
if ( false !== strpos( $uri, '?' ) ) {
|
||||
// remove the query string (we'll pull it from elsewhere later)
|
||||
$uri = urldecode( substr( $uri, 0, strpos( $uri, '?' ) ) );
|
||||
} else {
|
||||
$uri = urldecode( $uri );
|
||||
}
|
||||
$query_string = isset( $_SERVER['QUERY_STRING'] ) ? '?' . filter_var( wp_unslash( $_SERVER['QUERY_STRING'] ), FILTER_DEFAULT ) : '';
|
||||
if ( 1 === preg_match( '/^https?:\/\//', $uri ) ) {
|
||||
// sometimes $_SERVER[REQUEST_URI] already includes the full domain name
|
||||
$uri_host = substr( $uri, 0, strpos( $uri, '/', 8 ) );
|
||||
$uri_path = substr( $uri, strlen( $uri_host ) );
|
||||
$this->url = array( $uri_host, $uri_path, $query_string );
|
||||
} else {
|
||||
// otherwise build the URI manually
|
||||
$uri_scheme = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] )
|
||||
? 'https'
|
||||
: 'http';
|
||||
$uri_host = isset( $_SERVER['HTTP_HOST'] )
|
||||
? filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_DEFAULT )
|
||||
: (
|
||||
isset( $_SERVER['SERVER_NAME'] )
|
||||
? filter_var( wp_unslash( $_SERVER['SERVER_NAME'] ), FILTER_DEFAULT )
|
||||
: ''
|
||||
);
|
||||
$uri_port = isset( $_SERVER['SERVER_PORT'] )
|
||||
? filter_var( wp_unslash( $_SERVER['SERVER_PORT'] ), FILTER_SANITIZE_NUMBER_INT )
|
||||
: '';
|
||||
// we only need to include the port if it's non-standard
|
||||
if ( $uri_port && ( 'http' === $uri_scheme && '80' !== $uri_port || 'https' === $uri_scheme && '443' !== $uri_port ) ) {
|
||||
$uri_port = ':' . $uri_port;
|
||||
} else {
|
||||
$uri_port = '';
|
||||
}
|
||||
$this->url = array(
|
||||
$uri_scheme . '://' . $uri_host . $uri_port,
|
||||
$uri,
|
||||
$query_string,
|
||||
);
|
||||
}
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the requested URI
|
||||
*
|
||||
* @param boolean $include_host If true, the scheme and domain will be included in the returned string (i.e. 'https://wordpress.com/index.php).
|
||||
* If false, only the requested URI path will be returned (i.e. '/index.php').
|
||||
* @return string
|
||||
*/
|
||||
public function get_uri( $include_host = false ) {
|
||||
list( $host, $file, $query ) = $this->get_url();
|
||||
|
||||
return ( $include_host ? $host : '' ) . $file . $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filename part of the request
|
||||
*
|
||||
* @example for 'https://wordpress.com/some/page?id=5', return '/some/page'
|
||||
* @return string
|
||||
*/
|
||||
public function get_filename() {
|
||||
return $this->get_url()[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the basename part of the request
|
||||
*
|
||||
* @example for 'https://wordpress.com/some/page.php?id=5', return 'page.php'
|
||||
* @return string
|
||||
*/
|
||||
public function get_basename() {
|
||||
// Get the filename part of the request
|
||||
$filename = $this->get_filename();
|
||||
// Normalize slashes
|
||||
$filename = str_replace( '\\', '/', $filename );
|
||||
// Remove trailing slashes
|
||||
$filename = rtrim( $filename, '/' );
|
||||
// Return the basename
|
||||
$offset = strrpos( $filename, '/' );
|
||||
return $offset !== false ? substr( $filename, $offset + 1 ) : $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query string. If present, it will be prefixed with '?'. Otherwise, it will be an empty string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_query_string() {
|
||||
return $this->get_url()[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request body.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_body() {
|
||||
$body = file_get_contents( 'php://input' );
|
||||
return false === $body ? '' : $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cookies
|
||||
*
|
||||
* @return array{string, scalar}[]
|
||||
*/
|
||||
public function get_cookies() {
|
||||
return flatten_array( $_COOKIE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the GET variables
|
||||
*
|
||||
* @return array{string, scalar}[]
|
||||
*/
|
||||
public function get_get_vars() {
|
||||
return flatten_array( $_GET );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the POST variables from a JSON body
|
||||
*
|
||||
* @return array{string, scalar}[]
|
||||
*/
|
||||
private function get_json_post_vars() {
|
||||
$decoded_json = json_decode( $this->get_body(), true ) ?? array();
|
||||
return flatten_array( $decoded_json, 'json', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the POST variables from a urlencoded body
|
||||
*
|
||||
* @return array{string, scalar}[]
|
||||
*/
|
||||
private function get_urlencoded_post_vars() {
|
||||
parse_str( $this->get_body(), $params );
|
||||
return flatten_array( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the POST variables
|
||||
*
|
||||
* @param string $body_processor Manually specifiy the method to use to process the body. Options are 'URLENCODED' and 'JSON'.
|
||||
*
|
||||
* @return array{string, scalar}[]
|
||||
*/
|
||||
public function get_post_vars( string $body_processor = '' ) {
|
||||
$content_type = $this->get_header( 'content-type' );
|
||||
|
||||
// If the body processor is specified by the rules file, trust it.
|
||||
if ( 'URLENCODED' === $body_processor ) {
|
||||
return $this->get_urlencoded_post_vars();
|
||||
}
|
||||
if ( 'JSON' === $body_processor ) {
|
||||
return $this->get_json_post_vars();
|
||||
}
|
||||
|
||||
// Otherwise, use $_POST if it's not empty.
|
||||
if ( ! empty( $_POST ) ) {
|
||||
return flatten_array( $_POST );
|
||||
}
|
||||
|
||||
// Lastly, try to parse the body based on the content type.
|
||||
if ( strpos( $content_type, 'application/json' ) !== false ) {
|
||||
return $this->get_json_post_vars();
|
||||
}
|
||||
if ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) {
|
||||
return $this->get_urlencoded_post_vars();
|
||||
}
|
||||
|
||||
// Don't try to parse any other content types.
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the files that were uploaded with this request (i.e. what's in the $_FILES superglobal)
|
||||
*
|
||||
* @return RequestFile[]
|
||||
*/
|
||||
public function get_files() {
|
||||
$files = array();
|
||||
foreach ( $_FILES as $field_name => $arr ) {
|
||||
// flatten the values in case we were given inputs with brackets
|
||||
foreach ( flatten_array( $arr ) as list( $arr_key, $arr_value ) ) {
|
||||
if ( $arr_key === 'name' ) {
|
||||
// if this file was a simple (non-nested) name and unique, then just add it.
|
||||
$files[] = array(
|
||||
'name' => $field_name,
|
||||
'filename' => $arr_value,
|
||||
);
|
||||
} elseif ( 'name[' === substr( $arr_key, 0, 5 ) ) {
|
||||
// otherwise this was a file with a nested name and/or multiple files with the same name
|
||||
$files[] = array(
|
||||
'name' => $field_name . substr( $arr_key, 4 ),
|
||||
'filename' => $arr_value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for generating and working with firewall rule files.
|
||||
*
|
||||
* @since 0.9.0
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\IP\Utils as IP_Utils;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Class for generating and working with firewall rule files.
|
||||
*/
|
||||
class Waf_Rules_Manager {
|
||||
|
||||
const RULES_VERSION = '1.0.0';
|
||||
|
||||
// WAF Options
|
||||
const VERSION_OPTION_NAME = 'jetpack_waf_rules_version';
|
||||
const AUTOMATIC_RULES_ENABLED_OPTION_NAME = 'jetpack_waf_automatic_rules';
|
||||
const IP_ALLOW_LIST_OPTION_NAME = 'jetpack_waf_ip_allow_list';
|
||||
const IP_ALLOW_LIST_ENABLED_OPTION_NAME = 'jetpack_waf_ip_allow_list_enabled';
|
||||
const IP_BLOCK_LIST_OPTION_NAME = 'jetpack_waf_ip_block_list';
|
||||
const IP_BLOCK_LIST_ENABLED_OPTION_NAME = 'jetpack_waf_ip_block_list_enabled';
|
||||
const RULE_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_last_updated_timestamp';
|
||||
const AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_automatic_rules_last_updated_timestamp';
|
||||
|
||||
/**
|
||||
* IP Lists Enabled Option Name
|
||||
*
|
||||
* @deprecated 0.17.0 Use Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME and Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME instead.
|
||||
*/
|
||||
const IP_LISTS_ENABLED_OPTION_NAME = 'jetpack_waf_ip_list';
|
||||
|
||||
// Rule Files
|
||||
const AUTOMATIC_RULES_FILE = '/rules/automatic-rules.php';
|
||||
const IP_ALLOW_RULES_FILE = '/rules/allow-ip.php';
|
||||
const IP_BLOCK_RULES_FILE = '/rules/block-ip.php';
|
||||
|
||||
/**
|
||||
* Rules Entrypoint File
|
||||
*
|
||||
* @deprecated 0.22.0 Use JETPACK_WAF_ENTRYPOINT instead.
|
||||
*/
|
||||
const RULES_ENTRYPOINT_FILE = '/rules/rules.php';
|
||||
|
||||
/**
|
||||
* Whether automatic rules are enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function automatic_rules_enabled() {
|
||||
return (bool) get_option( self::AUTOMATIC_RULES_ENABLED_OPTION_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether IP allow list is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function ip_allow_list_enabled() {
|
||||
return (bool) get_option( self::IP_ALLOW_LIST_ENABLED_OPTION_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether IP block list is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function ip_block_list_enabled() {
|
||||
return (bool) get_option( self::IP_BLOCK_LIST_ENABLED_OPTION_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks for the WAF rules.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_hooks() {
|
||||
// Re-activate the WAF any time an option is added or updated.
|
||||
add_action( 'add_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'update_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'add_option_' . self::IP_ALLOW_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'update_option_' . self::IP_ALLOW_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'add_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'update_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'add_option_' . self::IP_BLOCK_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'update_option_' . self::IP_BLOCK_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'add_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
add_action( 'update_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
|
||||
// Register the cron job.
|
||||
add_action( 'jetpack_waf_rules_update_cron', array( static::class, 'update_rules_cron' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the cron job to update the WAF rules.
|
||||
*
|
||||
* @return bool|WP_Error True if the event is scheduled, WP_Error on failure.
|
||||
*/
|
||||
public static function schedule_rules_cron() {
|
||||
if ( ! wp_next_scheduled( 'jetpack_waf_rules_update_cron' ) ) {
|
||||
return wp_schedule_event( time(), 'twicedaily', 'jetpack_waf_rules_update_cron', array(), true );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries periodically to update the rules using our API.
|
||||
*
|
||||
* @return bool|WP_Error True if rules update is successful, WP_Error on failure.
|
||||
*/
|
||||
public static function update_rules_cron() {
|
||||
try {
|
||||
self::generate_automatic_rules();
|
||||
self::generate_ip_rules();
|
||||
self::generate_rules();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
|
||||
update_option( self::RULE_LAST_UPDATED_OPTION_NAME, time() );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-activate the WAF any time an option is added or updated.
|
||||
*
|
||||
* @return bool|WP_Error True if re-activation is successful, WP_Error on failure.
|
||||
*/
|
||||
public static function reactivate_on_rules_option_change() {
|
||||
try {
|
||||
Waf_Runner::activate();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return $e->get_wp_error();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the rule set if rules version has changed
|
||||
*
|
||||
* @throws Waf_Exception If the firewall mode is invalid.
|
||||
* @throws Waf_Exception If the rules update fails.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function update_rules_if_changed() {
|
||||
$version = get_option( self::VERSION_OPTION_NAME );
|
||||
if ( self::RULES_VERSION !== $version ) {
|
||||
self::generate_automatic_rules();
|
||||
self::generate_ip_rules();
|
||||
self::generate_rules();
|
||||
|
||||
update_option( self::VERSION_OPTION_NAME, self::RULES_VERSION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve rules from the API
|
||||
*
|
||||
* @throws Waf_Exception If site is not registered.
|
||||
* @throws Rules_API_Exception If API did not respond 200.
|
||||
* @throws Rules_API_Exception If data is missing from response.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_rules_from_api() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
if ( ! $blog_id ) {
|
||||
throw new Waf_Exception( 'Site is not registered' );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%s/waf-rules', $blog_id ),
|
||||
'2',
|
||||
array(),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( 200 !== $response_code ) {
|
||||
throw new Rules_API_Exception( 'API connection failed.', (int) $response_code );
|
||||
}
|
||||
|
||||
$rules_json = wp_remote_retrieve_body( $response );
|
||||
$rules = json_decode( $rules_json, true );
|
||||
|
||||
if ( empty( $rules['data'] ) ) {
|
||||
throw new Rules_API_Exception( 'Data missing from response.' );
|
||||
}
|
||||
|
||||
return $rules['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a require statement in a file_exists check.
|
||||
*
|
||||
* @param string $required_file The file to check if exists and require.
|
||||
* @param string $return_code The PHP code to execute if the file require returns true. Defaults to 'return;'.
|
||||
*
|
||||
* @return string The wrapped require statement.
|
||||
*/
|
||||
private static function wrap_require( $required_file, $return_code = 'return;' ) {
|
||||
return "if ( file_exists( '$required_file' ) ) { if ( require( '$required_file' ) ) { $return_code } }";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the rules.php script
|
||||
*
|
||||
* @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction.
|
||||
*
|
||||
* @throws File_System_Exception If file writing fails initializing rule files.
|
||||
* @throws File_System_Exception If file writing fails writing to the rules entrypoint file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function generate_rules() {
|
||||
global $wp_filesystem;
|
||||
Waf_Runner::initialize_filesystem();
|
||||
Waf_Constants::define_entrypoint();
|
||||
|
||||
$rules = "<?php\n";
|
||||
$entrypoint_file_path = Waf_Runner::get_waf_file_path( JETPACK_WAF_ENTRYPOINT );
|
||||
|
||||
// Ensure that the folder exists
|
||||
if ( ! $wp_filesystem->is_dir( dirname( $entrypoint_file_path ) ) ) {
|
||||
$wp_filesystem->mkdir( dirname( $entrypoint_file_path ) );
|
||||
}
|
||||
|
||||
// Ensure all potentially required rule files exist
|
||||
$rule_files = array( JETPACK_WAF_ENTRYPOINT, self::AUTOMATIC_RULES_FILE, self::IP_ALLOW_RULES_FILE, self::IP_BLOCK_RULES_FILE );
|
||||
foreach ( $rule_files as $rule_file ) {
|
||||
$rule_file = Waf_Runner::get_waf_file_path( $rule_file );
|
||||
if ( ! $wp_filesystem->is_file( $rule_file ) ) {
|
||||
if ( ! $wp_filesystem->put_contents( $rule_file, "<?php\n" ) ) {
|
||||
throw new File_System_Exception( 'Failed writing rules file to: ' . $rule_file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add IP allow list
|
||||
if ( self::ip_allow_list_enabled() ) {
|
||||
$rules .= self::wrap_require( Waf_Runner::get_waf_file_path( self::IP_ALLOW_RULES_FILE ) ) . "\n";
|
||||
}
|
||||
|
||||
// Add IP block list
|
||||
if ( self::ip_block_list_enabled() ) {
|
||||
$rules .= self::wrap_require( Waf_Runner::get_waf_file_path( self::IP_BLOCK_RULES_FILE ), "return \$waf->block( 'block', -1, 'ip block list' );" ) . "\n";
|
||||
}
|
||||
|
||||
// Add automatic rules
|
||||
if ( self::automatic_rules_enabled() ) {
|
||||
$rules .= self::wrap_require( Waf_Runner::get_waf_file_path( self::AUTOMATIC_RULES_FILE ) ) . "\n";
|
||||
}
|
||||
|
||||
// Update the rules file
|
||||
if ( ! $wp_filesystem->put_contents( $entrypoint_file_path, $rules ) ) {
|
||||
throw new File_System_Exception( 'Failed writing rules file to: ' . $entrypoint_file_path );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the automatic-rules.php script
|
||||
*
|
||||
* @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction.
|
||||
*
|
||||
* @throws Waf_Exception If rules cannot be fetched from the API.
|
||||
* @throws File_System_Exception If file writing fails.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function generate_automatic_rules() {
|
||||
global $wp_filesystem;
|
||||
Waf_Runner::initialize_filesystem();
|
||||
|
||||
$automatic_rules_file_path = Waf_Runner::get_waf_file_path( self::AUTOMATIC_RULES_FILE );
|
||||
|
||||
// Ensure that the folder exists.
|
||||
if ( ! $wp_filesystem->is_dir( dirname( $automatic_rules_file_path ) ) ) {
|
||||
$wp_filesystem->mkdir( dirname( $automatic_rules_file_path ) );
|
||||
}
|
||||
|
||||
try {
|
||||
$rules = self::get_rules_from_api();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
// Do not throw API exceptions for users who do not have access
|
||||
if ( 401 !== $e->getCode() ) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no rules available, don't overwrite the existing file.
|
||||
if ( empty( $rules ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $wp_filesystem->put_contents( $automatic_rules_file_path, $rules ) ) {
|
||||
throw new File_System_Exception( 'Failed writing automatic rules file to: ' . $automatic_rules_file_path );
|
||||
}
|
||||
|
||||
update_option( self::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME, time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the rules.php script
|
||||
*
|
||||
* @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction.
|
||||
*
|
||||
* @throws File_System_Exception If writing to IP allow list file fails.
|
||||
* @throws File_System_Exception If writing to IP block list file fails.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function generate_ip_rules() {
|
||||
global $wp_filesystem;
|
||||
Waf_Runner::initialize_filesystem();
|
||||
|
||||
$allow_ip_file_path = Waf_Runner::get_waf_file_path( self::IP_ALLOW_RULES_FILE );
|
||||
$block_ip_file_path = Waf_Runner::get_waf_file_path( self::IP_BLOCK_RULES_FILE );
|
||||
|
||||
// Ensure that the folders exists.
|
||||
if ( ! $wp_filesystem->is_dir( dirname( $allow_ip_file_path ) ) ) {
|
||||
$wp_filesystem->mkdir( dirname( $allow_ip_file_path ) );
|
||||
}
|
||||
if ( ! $wp_filesystem->is_dir( dirname( $block_ip_file_path ) ) ) {
|
||||
$wp_filesystem->mkdir( dirname( $block_ip_file_path ) );
|
||||
}
|
||||
|
||||
$allow_list = IP_Utils::get_ip_addresses_from_string( get_option( self::IP_ALLOW_LIST_OPTION_NAME ) );
|
||||
$block_list = IP_Utils::get_ip_addresses_from_string( get_option( self::IP_BLOCK_LIST_OPTION_NAME ) );
|
||||
|
||||
$allow_rules_content = '';
|
||||
// phpcs:disable WordPress.PHP.DevelopmentFunctions
|
||||
$allow_rules_content .= '$waf_allow_list = ' . var_export( $allow_list, true ) . ";\n";
|
||||
// phpcs:enable
|
||||
$allow_rules_content .= 'return $waf->is_ip_in_array( $waf_allow_list );' . "\n";
|
||||
|
||||
if ( ! $wp_filesystem->put_contents( $allow_ip_file_path, "<?php\n$allow_rules_content" ) ) {
|
||||
throw new File_System_Exception( 'Failed writing allow list file to: ' . $allow_ip_file_path );
|
||||
}
|
||||
|
||||
$block_rules_content = '';
|
||||
// phpcs:disable WordPress.PHP.DevelopmentFunctions
|
||||
$block_rules_content .= '$waf_block_list = ' . var_export( $block_list, true ) . ";\n";
|
||||
// phpcs:enable
|
||||
$block_rules_content .= 'return $waf->is_ip_in_array( $waf_block_list );' . "\n";
|
||||
|
||||
if ( ! $wp_filesystem->put_contents( $block_ip_file_path, "<?php\n$block_rules_content" ) ) {
|
||||
throw new File_System_Exception( 'Failed writing block list file to: ' . $block_ip_file_path );
|
||||
}
|
||||
}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
/**
|
||||
* Entrypoint for actually executing the WAF.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\Modules;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
|
||||
|
||||
/**
|
||||
* Executes the WAF.
|
||||
*/
|
||||
class Waf_Runner {
|
||||
|
||||
const WAF_MODULE_NAME = 'waf';
|
||||
const MODE_OPTION_NAME = 'jetpack_waf_mode';
|
||||
const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
|
||||
const SHARE_DEBUG_DATA_OPTION_NAME = 'jetpack_waf_share_debug_data';
|
||||
|
||||
/**
|
||||
* Run the WAF
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function initialize() {
|
||||
if ( ! self::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
Waf_Constants::define_mode();
|
||||
Waf_Constants::define_entrypoint();
|
||||
Waf_Constants::define_share_data();
|
||||
|
||||
if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
|
||||
return;
|
||||
}
|
||||
// Don't run if in standalone mode
|
||||
if ( function_exists( 'add_action' ) ) {
|
||||
self::add_hooks();
|
||||
Waf_Rules_Manager::add_hooks();
|
||||
Waf_Rules_Manager::schedule_rules_cron();
|
||||
}
|
||||
if ( ! self::did_run() ) {
|
||||
self::run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set action hooks
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_hooks() {
|
||||
// Register REST routes. Use a static callable so the controller class is not
|
||||
// loaded into memory/opcache on requests that never reach `rest_api_init`.
|
||||
add_action( 'rest_api_init', array( REST_Controller::class, 'register_rest_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Did the WAF run yet or not?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function did_run() {
|
||||
return defined( 'JETPACK_WAF_RUN' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the passed $option is one of the allowed WAF operation modes.
|
||||
*
|
||||
* @param string $option The mode option.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_allowed_mode( $option ) {
|
||||
// Normal constants are defined prior to WP_CLI running causing problems for activation
|
||||
if ( defined( 'WAF_CLI_MODE' ) ) {
|
||||
$option = WAF_CLI_MODE;
|
||||
}
|
||||
|
||||
$allowed_modes = array(
|
||||
'normal',
|
||||
'silent',
|
||||
);
|
||||
|
||||
return in_array( $option, $allowed_modes, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the WAF is supported in the current environment.
|
||||
*
|
||||
* @since 0.8.0
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_supported_environment() {
|
||||
// Do not run when killswitch is enabled
|
||||
if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( defined( 'IS_ATOMIC_JN' ) && IS_ATOMIC_JN ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not run in the WPCOM context
|
||||
if ( ( new Host() )->is_wpcom_simple() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not run on the Atomic platform
|
||||
if ( ( new Host() )->is_atomic_platform() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not run on the VIP platform
|
||||
if ( ( new Host() )->is_vip_site() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the WAF module is enabled on the site.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
// if ABSPATH is defined, then WordPress has already been instantiated,
|
||||
// so we can check to see if the waf module is activated.
|
||||
if ( defined( 'ABSPATH' ) ) {
|
||||
return ( new Modules() )->is_active( self::WAF_MODULE_NAME );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the WAF module on the site.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function enable() {
|
||||
return ( new Modules() )->activate( self::WAF_MODULE_NAME, false, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled the WAF module on the site.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function disable() {
|
||||
return ( new Modules() )->deactivate( self::WAF_MODULE_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Config
|
||||
*
|
||||
* @return array The WAF settings and current configuration data.
|
||||
*/
|
||||
public static function get_config() {
|
||||
return array(
|
||||
Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME => Waf_Rules_Manager::automatic_rules_enabled(),
|
||||
Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ),
|
||||
Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_allow_list_enabled(),
|
||||
Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME ),
|
||||
Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_block_list_enabled(),
|
||||
self::SHARE_DATA_OPTION_NAME => get_option( self::SHARE_DATA_OPTION_NAME ),
|
||||
self::SHARE_DEBUG_DATA_OPTION_NAME => get_option( self::SHARE_DEBUG_DATA_OPTION_NAME ),
|
||||
'bootstrap_path' => self::get_bootstrap_file_path(),
|
||||
'standalone_mode' => self::get_standalone_mode_status(),
|
||||
'automatic_rules_available' => (bool) self::automatic_rules_available(),
|
||||
'brute_force_protection' => (bool) Brute_Force_Protection::is_enabled(),
|
||||
|
||||
/**
|
||||
* Provide the deprecated IP lists options for backwards compatibility with older versions of the Jetpack and Protect plugins.
|
||||
* i.e. If one plugin is updated and the other is not, the latest version of this package will be used by both plugins.
|
||||
*
|
||||
* @deprecated 0.17.0
|
||||
*/
|
||||
// @phan-suppress-next-line PhanDeprecatedClassConstant -- Needed for backwards compatibility.
|
||||
Waf_Rules_Manager::IP_LISTS_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_allow_list_enabled() || Waf_Rules_Manager::ip_block_list_enabled(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bootstrap File Path
|
||||
*
|
||||
* @return string The path to the Jetpack Firewall's bootstrap.php file.
|
||||
*/
|
||||
private static function get_bootstrap_file_path() {
|
||||
$bootstrap = new Waf_Standalone_Bootstrap();
|
||||
return $bootstrap->get_bootstrap_file_path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF standalone mode status
|
||||
*
|
||||
* @return bool|array True if WAF standalone mode is enabled, false otherwise.
|
||||
*/
|
||||
public static function get_standalone_mode_status() {
|
||||
return defined( 'JETPACK_WAF_RUN' ) && JETPACK_WAF_RUN === 'preload';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF File Path
|
||||
*
|
||||
* @param string $file The file path starting in the WAF directory.
|
||||
* @return string The full file path to the provided file in the WAF directory.
|
||||
*/
|
||||
public static function get_waf_file_path( $file ) {
|
||||
Waf_Constants::define_waf_directory();
|
||||
|
||||
// Ensure the file path starts with a slash.
|
||||
if ( '/' !== substr( $file, 0, 1 ) ) {
|
||||
$file = "/$file";
|
||||
}
|
||||
|
||||
return JETPACK_WAF_DIR . $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the WAF and potentially stops the request if a problem is found.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function run() {
|
||||
// Make double-sure we are only running once.
|
||||
if ( self::did_run() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Waf_Constants::initialize_constants();
|
||||
|
||||
// if ABSPATH is defined, then WordPress has already been instantiated,
|
||||
// and we're running as a plugin (meh). Otherwise, we're running via something
|
||||
// like PHP's prepend_file setting (yay!).
|
||||
define( 'JETPACK_WAF_RUN', defined( 'ABSPATH' ) ? 'plugin' : 'preload' );
|
||||
|
||||
// If the WAF is being run before a command line script, or in any other non-HTTP
|
||||
// context (e.g. server-side cron executed via a PHP wrapper that does not report
|
||||
// PHP_SAPI as 'cli'), there is no HTTP request to evaluate. Skip rule execution so
|
||||
// HTTP-specific rules (e.g. rule 911100, which checks the request method) don't
|
||||
// produce a false-positive 403 block.
|
||||
if ( PHP_SAPI === 'cli' || ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if something terrible happens during the WAF running, we don't want to interfere with the rest of the site,
|
||||
// so we intercept errors ONLY while the WAF is running, then we remove our handler after the WAF finishes.
|
||||
$display_errors = ini_get( 'display_errors' );
|
||||
|
||||
ini_set( 'display_errors', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- We only customize error reporting while the WAF is running, and remove our handler afterwards.
|
||||
|
||||
set_error_handler( array( self::class, 'errorHandler' ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- We only customize error reporting while the WAF is running, and remove our handler afterwards.
|
||||
|
||||
try {
|
||||
|
||||
// phpcs:ignore
|
||||
$waf = new Waf_Runtime( new Waf_Transforms(), new Waf_Operators() );
|
||||
|
||||
// execute waf rules.
|
||||
$rules_file_path = self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT );
|
||||
if ( file_exists( $rules_file_path ) ) {
|
||||
include $rules_file_path;
|
||||
}
|
||||
} catch ( \Exception $err ) { // phpcs:ignore
|
||||
// Intentionally doing nothing.
|
||||
}
|
||||
|
||||
// remove the custom error handler, so we don't interfere with the site.
|
||||
restore_error_handler();
|
||||
|
||||
// Restore the original value.
|
||||
ini_set( 'display_errors', $display_errors ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- We only customize error reporting while the WAF is running, and remove our handler afterwards.
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handler to be used while the WAF is being executed.
|
||||
*
|
||||
* @param int $code The error code.
|
||||
* @param string $message The error message.
|
||||
* @param string $file File with the error.
|
||||
* @param string $line Line of the error.
|
||||
* @return void
|
||||
*/
|
||||
public static function errorHandler( $code, $message, $file, $line ) { // phpcs:ignore
|
||||
// Intentionally doing nothing for now.
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the WP filesystem and WAF directory structure.
|
||||
*
|
||||
* @throws File_System_Exception If filesystem is unavailable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function initialize_filesystem() {
|
||||
if ( ! function_exists( '\\WP_Filesystem' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
if ( ! \WP_Filesystem() ) {
|
||||
throw new File_System_Exception( 'No filesystem available.' );
|
||||
}
|
||||
|
||||
self::initialize_waf_directory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the WAF by generating the rules script and setting the version
|
||||
*
|
||||
* @throws Waf_Exception If the firewall mode is invalid.
|
||||
* @throws Waf_Exception If the activation fails.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate() {
|
||||
$version = get_option( Waf_Rules_Manager::VERSION_OPTION_NAME );
|
||||
if ( ! $version ) {
|
||||
add_option( Waf_Rules_Manager::VERSION_OPTION_NAME, Waf_Rules_Manager::RULES_VERSION );
|
||||
}
|
||||
|
||||
add_option( self::SHARE_DATA_OPTION_NAME, true );
|
||||
|
||||
self::initialize_filesystem();
|
||||
|
||||
Waf_Rules_Manager::generate_automatic_rules();
|
||||
Waf_Rules_Manager::generate_ip_rules();
|
||||
Waf_Rules_Manager::generate_rules();
|
||||
|
||||
Waf_Blocklog_Manager::create_blocklog_table();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the waf directory is created.
|
||||
*
|
||||
* @throws File_System_Exception If filesystem is unavailable.
|
||||
* @throws File_System_Exception If creating the directory fails.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function initialize_waf_directory() {
|
||||
WP_Filesystem();
|
||||
Waf_Constants::define_waf_directory();
|
||||
|
||||
global $wp_filesystem;
|
||||
if ( ! $wp_filesystem ) {
|
||||
throw new File_System_Exception( 'Cannot work without the file system being initialized.' );
|
||||
}
|
||||
|
||||
if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) {
|
||||
if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) {
|
||||
throw new File_System_Exception( 'Failed creating WAF file directory: ' . JETPACK_WAF_DIR );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the WAF by deleting the relevant options and emptying rules file.
|
||||
*
|
||||
* @throws File_System_Exception If file writing fails.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate() {
|
||||
delete_option( self::MODE_OPTION_NAME );
|
||||
delete_option( Waf_Rules_Manager::VERSION_OPTION_NAME );
|
||||
|
||||
global $wp_filesystem;
|
||||
self::initialize_filesystem();
|
||||
Waf_Constants::define_entrypoint();
|
||||
|
||||
// If the rules file doesn't exist, there's nothing else to do.
|
||||
if ( ! $wp_filesystem->exists( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty the rules entrypoint file.
|
||||
if ( ! $wp_filesystem->put_contents( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ), "<?php\n" ) ) {
|
||||
throw new File_System_Exception( 'Failed to empty rules.php file.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle updates to the WAF
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function update_waf() {
|
||||
Waf_Rules_Manager::update_rules_if_changed();
|
||||
|
||||
// Re-generate the standalone bootstrap file on every update
|
||||
// TODO: We may consider only doing this when the WAF version changes
|
||||
( new Waf_Standalone_Bootstrap() )->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an automatic rules file is available
|
||||
*
|
||||
* @return bool False if an automatic rules file is not available, true otherwise
|
||||
*/
|
||||
public static function automatic_rules_available() {
|
||||
$automatic_rules_last_updated = get_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
|
||||
|
||||
// If we do not have a automatic rules last updated timestamp cached, return false.
|
||||
if ( ! $automatic_rules_last_updated ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate that the automatic rules file exists and is not empty.
|
||||
global $wp_filesystem;
|
||||
|
||||
try {
|
||||
self::initialize_filesystem();
|
||||
} catch ( Waf_Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$automatic_rules_file_contents = $wp_filesystem->get_contents( self::get_waf_file_path( Waf_Rules_Manager::AUTOMATIC_RULES_FILE ) );
|
||||
|
||||
// If the automatic rules file was removed or is now empty, return false.
|
||||
if ( ! $automatic_rules_file_contents || "<?php\n" === $automatic_rules_file_contents ) {
|
||||
|
||||
// Delete the automatic rules last updated option.
|
||||
delete_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
|
||||
|
||||
// If automatic rules setting is enabled, disable it.
|
||||
if ( Waf_Rules_Manager::automatic_rules_enabled() ) {
|
||||
update_option( Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+860
@@ -0,0 +1,860 @@
|
||||
<?php
|
||||
/**
|
||||
* Runtime for Jetpack Waf
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\IP\Utils as IP_Utils;
|
||||
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This class is all about sanitizing input.
|
||||
|
||||
/**
|
||||
* The environment variable that defined the WAF running mode.
|
||||
*
|
||||
* @var string JETPACK_WAF_MODE
|
||||
*/
|
||||
|
||||
// Type aliases for this file.
|
||||
<<<'PHAN'
|
||||
@phan-type Target = array{ only?: string[], except?: string[], count?: boolean }
|
||||
@phan-type TargetBag = array<string, Target>
|
||||
PHAN;
|
||||
|
||||
/**
|
||||
* Waf_Runtime class
|
||||
*/
|
||||
class Waf_Runtime {
|
||||
/**
|
||||
* If used, normalize_array_targets() will just return the number of matching values, instead of the values themselves.
|
||||
*/
|
||||
const NORMALIZE_ARRAY_COUNT = 1;
|
||||
/**
|
||||
* If used, normalize_array_targets() will apply "only" and "except" filters to the values of the source array, instead of the keys.
|
||||
*/
|
||||
const NORMALIZE_ARRAY_MATCH_VALUES = 2;
|
||||
|
||||
/**
|
||||
* The version of this runtime class. Used by rule files to ensure compatibility.
|
||||
*
|
||||
* @since 0.21.0
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $version = 1;
|
||||
/**
|
||||
* Last rule.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $last_rule = '';
|
||||
/**
|
||||
* Matched vars.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $matched_vars = array();
|
||||
/**
|
||||
* Matched var.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $matched_var = '';
|
||||
/**
|
||||
* Matched var names.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $matched_vars_names = array();
|
||||
/**
|
||||
* Matched var name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $matched_var_name = '';
|
||||
/**
|
||||
* Body Processor.
|
||||
*
|
||||
* @var string 'URLENCODED' | 'JSON' | ''
|
||||
*/
|
||||
private $body_processor = '';
|
||||
|
||||
/**
|
||||
* State.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $state = array();
|
||||
/**
|
||||
* Metadata.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $metadata = array();
|
||||
|
||||
/**
|
||||
* Transforms.
|
||||
*
|
||||
* @var Waf_Transforms
|
||||
*/
|
||||
private $transforms;
|
||||
/**
|
||||
* Operators.
|
||||
*
|
||||
* @var Waf_Operators
|
||||
*/
|
||||
private $operators;
|
||||
|
||||
/**
|
||||
* The request
|
||||
*
|
||||
* @var Waf_Request
|
||||
*/
|
||||
private $request;
|
||||
|
||||
/**
|
||||
* Rules to remove.
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
private $rules_to_remove = array(
|
||||
'id' => array(),
|
||||
'tag' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Targets to remove.
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
private $targets_to_remove = array(
|
||||
'id' => array(),
|
||||
'tag' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*
|
||||
* @param Waf_Transforms $transforms Transforms.
|
||||
* @param Waf_Operators $operators Operators.
|
||||
* @param ?Waf_Request $request Information about the request.
|
||||
*/
|
||||
public function __construct( $transforms, $operators, $request = null ) {
|
||||
$this->transforms = $transforms;
|
||||
$this->operators = $operators;
|
||||
$this->request = null === $request
|
||||
? new Waf_Request()
|
||||
: $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rule removed method.
|
||||
*
|
||||
* @param string $id Ids.
|
||||
* @param string[] $tags Tags.
|
||||
*/
|
||||
public function rule_removed( $id, $tags ) {
|
||||
if ( isset( $this->rules_to_remove['id'][ $id ] ) ) {
|
||||
return true;
|
||||
}
|
||||
foreach ( $tags as $tag ) {
|
||||
if ( isset( $this->rules_to_remove['tag'][ $tag ] ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Targets.
|
||||
*
|
||||
* @param array $targets Targets.
|
||||
* @param string $rule_id Rule id.
|
||||
* @param string[] $rule_tags Rule tags.
|
||||
*/
|
||||
public function update_targets( $targets, $rule_id, $rule_tags ) {
|
||||
$updates = array();
|
||||
// look for target updates based on the rule's ID.
|
||||
if ( isset( $this->targets_to_remove['id'][ $rule_id ] ) ) {
|
||||
foreach ( $this->targets_to_remove['id'][ $rule_id ] as $name => $props ) {
|
||||
$updates[] = array( $name, $props );
|
||||
}
|
||||
}
|
||||
// look for target updates based on the rule's tags.
|
||||
foreach ( $rule_tags as $tag ) {
|
||||
if ( isset( $this->targets_to_remove['tag'][ $tag ] ) ) {
|
||||
foreach ( $this->targets_to_remove['tag'][ $tag ] as $name => $props ) {
|
||||
$updates[] = array( $name, $props );
|
||||
}
|
||||
}
|
||||
}
|
||||
// apply any found target updates.
|
||||
|
||||
foreach ( $updates as list( $name, $props ) ) {
|
||||
if ( isset( $targets[ $name ] ) ) {
|
||||
// we only need to remove targets that exist.
|
||||
if ( true === $props ) {
|
||||
// if the entire target is being removed, remove it.
|
||||
unset( $targets[ $name ] );
|
||||
} else {
|
||||
// otherwise just mark single props to ignore.
|
||||
$targets[ $name ]['except'] = array_merge(
|
||||
$targets[ $name ]['except'] ?? array(),
|
||||
$props
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if at least one of the targets matches the rule.
|
||||
*
|
||||
* @param string[] $transforms One of the transform methods defined in the Jetpack Waf_Transforms class.
|
||||
* @param TargetBag $targets Targets.
|
||||
* @param string $match_operator Match operator.
|
||||
* @param mixed $match_value Match value.
|
||||
* @param bool $match_not Match not.
|
||||
* @param bool $capture Capture.
|
||||
* @return bool
|
||||
*/
|
||||
public function match_targets( $transforms, $targets, $match_operator, $match_value, $match_not, $capture = false ) {
|
||||
$match_found = false;
|
||||
|
||||
// get values.
|
||||
$values = $this->normalize_targets( $targets );
|
||||
|
||||
// apply transforms.
|
||||
foreach ( $transforms as $t ) {
|
||||
foreach ( $values as &$v ) {
|
||||
$v['value'] = $this->transforms->$t( $v['value'] );
|
||||
}
|
||||
}
|
||||
unset( $v );
|
||||
// pass each target value to the operator to find any that match.
|
||||
$matched = array();
|
||||
$captures = array();
|
||||
foreach ( $values as $v ) {
|
||||
$match = $this->operators->{$match_operator}( $v['value'], $match_value );
|
||||
$did_match = false !== $match;
|
||||
if ( $match_not !== $did_match ) {
|
||||
// If either:
|
||||
// - rule is negated ("not" flag set) and the target was not matched
|
||||
// - rule not negated and the target was matched
|
||||
// then this is considered a match.
|
||||
$match_found = true;
|
||||
$this->matched_vars_names[] = $v['name'];
|
||||
$this->matched_vars[] = $v['value'];
|
||||
$this->matched_var_name = end( $this->matched_vars_names );
|
||||
$this->matched_var = end( $this->matched_vars );
|
||||
$matched[] = array( $v, $match );
|
||||
// Set any captured matches into state if the rule has the "capture" flag.
|
||||
if ( $capture ) {
|
||||
$captures = is_array( $match ) ? $match : array( $match );
|
||||
foreach ( array_slice( $captures, 0, 10 ) as $i => $c ) {
|
||||
$this->set_var( "tx.$i", $c );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $match_found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a secure hash for an IP address.
|
||||
*
|
||||
* @param string $ip IP address.
|
||||
* @return string Hashed IP.
|
||||
*/
|
||||
private function get_ip_hash( string $ip ): string {
|
||||
$hash_key = wp_salt( 'auth' );
|
||||
return hash_hmac( 'sha256', $ip, $hash_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the IP is allowed for recovery.
|
||||
*
|
||||
* @param string $ip IP address.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_ip_allowed_for_recovery( string $ip ): bool {
|
||||
$allow_hash = get_transient( 'jetpack_waf_recovery_' . $ip );
|
||||
return $allow_hash && hash_equals( $allow_hash, $this->get_ip_hash( $ip ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a recovery attempt.
|
||||
*
|
||||
* @param string $real_ip The real IP address of the request.
|
||||
*/
|
||||
private function allow_login_or_prompt_recovery( $real_ip ) {
|
||||
$blocked_login_page = Waf_Blocked_Login_Page::instance( $real_ip );
|
||||
|
||||
if ( $blocked_login_page->is_blocked_user_valid() ) {
|
||||
// Allow the IP to bypass the block for 15 minutes.
|
||||
set_transient( 'jetpack_waf_recovery_' . $real_ip, $this->get_ip_hash( $real_ip ), 15 * 60 );
|
||||
return;
|
||||
}
|
||||
|
||||
$blocked_login_page->render_and_die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Block.
|
||||
*
|
||||
* @param string $action Action.
|
||||
* @param string $rule_id Rule id.
|
||||
* @param string $reason Block reason.
|
||||
* @param int $status_code Http status code.
|
||||
*/
|
||||
public function block( $action, $rule_id, $reason, $status_code = 403 ) {
|
||||
if ( 'ip block list' === $reason ) {
|
||||
$real_ip = $this->request->get_real_user_ip_address();
|
||||
|
||||
if ( $this->is_ip_allowed_for_recovery( $real_ip ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $pagenow;
|
||||
if ( isset( $pagenow ) && 'wp-login.php' === $pagenow ) {
|
||||
$this->allow_login_or_prompt_recovery( $real_ip );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $reason ) {
|
||||
$reason = "rule $rule_id";
|
||||
} else {
|
||||
$reason = $this->sanitize_output( $reason );
|
||||
}
|
||||
|
||||
Waf_Blocklog_Manager::write_blocklog( $rule_id, $reason );
|
||||
error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" );
|
||||
header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" );
|
||||
if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) {
|
||||
$protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP';
|
||||
header( $protocol . ' 403 Forbidden', true, $status_code );
|
||||
die( "rule $rule_id - reason $reason" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect.
|
||||
*
|
||||
* @param string $rule_id Rule id.
|
||||
* @param string $url Url.
|
||||
* @return never
|
||||
*/
|
||||
public function redirect( $rule_id, $url ) {
|
||||
error_log( "Jetpack WAF Redirected Request.\tRule:$rule_id\t$url" );
|
||||
header( "Location: $url" );
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag rule for removal.
|
||||
*
|
||||
* @param string $prop Prop.
|
||||
* @param string $value Value.
|
||||
*/
|
||||
public function flag_rule_for_removal( $prop, $value ) {
|
||||
if ( 'id' === $prop ) {
|
||||
$this->rules_to_remove['id'][ $value ] = true;
|
||||
} else {
|
||||
$this->rules_to_remove['tag'][ $value ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag target for removal.
|
||||
*
|
||||
* @param string $id_or_tag Id or tag.
|
||||
* @param string $id_or_tag_value Id or tag value.
|
||||
* @param string $name Name.
|
||||
* @param string $prop Prop.
|
||||
*/
|
||||
public function flag_target_for_removal( $id_or_tag, $id_or_tag_value, $name, $prop = null ) {
|
||||
if ( null === $prop ) {
|
||||
$this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] = true;
|
||||
} elseif (
|
||||
! isset( $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] )
|
||||
// if the entire target is already being removed then it would be redundant to remove a single property.
|
||||
|| true !== $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ]
|
||||
) {
|
||||
$this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ][] = $prop;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variable value.
|
||||
*
|
||||
* @param string $key Key.
|
||||
*/
|
||||
public function get_var( $key ) {
|
||||
return $this->state[ $key ] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set variable value.
|
||||
*
|
||||
* @param string $key Key.
|
||||
* @param string $value Value.
|
||||
*/
|
||||
public function set_var( $key, $value ) {
|
||||
$this->state[ $key ] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment variable.
|
||||
*
|
||||
* @param string $key Key.
|
||||
* @param mixed $value Value.
|
||||
*/
|
||||
public function inc_var( $key, $value ) {
|
||||
if ( ! isset( $this->state[ $key ] ) ) {
|
||||
$this->state[ $key ] = 0;
|
||||
}
|
||||
$this->state[ $key ] += floatval( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement variable.
|
||||
*
|
||||
* @param string $key Key.
|
||||
* @param mixed $value Value.
|
||||
*/
|
||||
public function dec_var( $key, $value ) {
|
||||
if ( ! isset( $this->state[ $key ] ) ) {
|
||||
$this->state[ $key ] = 0;
|
||||
}
|
||||
$this->state[ $key ] -= floatval( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset variable.
|
||||
*
|
||||
* @param string $key Key.
|
||||
*/
|
||||
public function unset_var( $key ) {
|
||||
unset( $this->state[ $key ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* A cache of metadata about the incoming request.
|
||||
*
|
||||
* @param string $key The type of metadata to request ('headers', 'request_method', etc.).
|
||||
*/
|
||||
public function meta( $key ) {
|
||||
if ( ! isset( $this->metadata[ $key ] ) ) {
|
||||
$value = null;
|
||||
switch ( $key ) {
|
||||
case 'headers':
|
||||
$value = $this->request->get_headers();
|
||||
break;
|
||||
case 'headers_names':
|
||||
$value = $this->args_names( $this->meta( 'headers' ) );
|
||||
break;
|
||||
case 'request_method':
|
||||
$value = $this->request->get_method();
|
||||
break;
|
||||
case 'request_protocol':
|
||||
$value = $this->request->get_protocol();
|
||||
break;
|
||||
case 'request_uri':
|
||||
$value = $this->request->get_uri( false );
|
||||
break;
|
||||
case 'request_uri_raw':
|
||||
$value = $this->request->get_uri( true );
|
||||
break;
|
||||
case 'request_filename':
|
||||
$value = $this->request->get_filename();
|
||||
break;
|
||||
case 'request_line':
|
||||
$value = sprintf(
|
||||
'%s %s %s',
|
||||
$this->request->get_method(),
|
||||
$this->request->get_uri( false ),
|
||||
$this->request->get_protocol()
|
||||
);
|
||||
break;
|
||||
case 'request_basename':
|
||||
$value = $this->request->get_basename();
|
||||
break;
|
||||
case 'request_body':
|
||||
$value = $this->request->get_body();
|
||||
break;
|
||||
case 'query_string':
|
||||
$value = $this->request->get_query_string();
|
||||
break;
|
||||
case 'args_get':
|
||||
$value = $this->request->get_get_vars();
|
||||
break;
|
||||
case 'args_get_names':
|
||||
$value = $this->args_names( $this->meta( 'args_get' ) );
|
||||
break;
|
||||
case 'args_post':
|
||||
$value = $this->request->get_post_vars( $this->get_body_processor() );
|
||||
break;
|
||||
case 'args_post_names':
|
||||
$value = $this->args_names( $this->meta( 'args_post' ) );
|
||||
break;
|
||||
case 'args':
|
||||
$value = array_merge( $this->meta( 'args_get' ), $this->meta( 'args_post' ) );
|
||||
break;
|
||||
case 'args_names':
|
||||
$value = $this->args_names( $this->meta( 'args' ) );
|
||||
break;
|
||||
case 'request_cookies':
|
||||
$value = $this->request->get_cookies();
|
||||
break;
|
||||
case 'request_cookies_names':
|
||||
$value = $this->args_names( $this->meta( 'request_cookies' ) );
|
||||
break;
|
||||
case 'files':
|
||||
$value = array();
|
||||
foreach ( $this->request->get_files() as $f ) {
|
||||
$value[] = array( $f['name'], $f['filename'] );
|
||||
}
|
||||
break;
|
||||
case 'files_names':
|
||||
$value = $this->args_names( $this->meta( 'files' ) );
|
||||
break;
|
||||
case 'matched_vars':
|
||||
$value = array_combine( $this->matched_vars_names, $this->matched_vars );
|
||||
break;
|
||||
case 'matched_var':
|
||||
$value = array( $this->matched_var_name => $this->matched_var );
|
||||
break;
|
||||
case 'matched_vars_names':
|
||||
$value = $this->matched_vars_names;
|
||||
break;
|
||||
case 'matched_var_name':
|
||||
$value = array( $this->matched_var_name );
|
||||
break;
|
||||
}
|
||||
$this->metadata[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $this->metadata[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* State values.
|
||||
*
|
||||
* @param string $prefix Prefix.
|
||||
*/
|
||||
private function state_values( $prefix ) {
|
||||
$output = array();
|
||||
$len = strlen( $prefix );
|
||||
foreach ( $this->state as $k => $v ) {
|
||||
if ( 0 === stripos( $k, $prefix ) ) {
|
||||
$output[ substr( $k, $len ) ] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the body processor.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_body_processor() {
|
||||
return $this->body_processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the body processor.
|
||||
*
|
||||
* @param string $processor Processor to set. Either 'URLENCODED' or 'JSON'.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_body_processor( $processor ) {
|
||||
if ( $processor === 'URLENCODED' || $processor === 'JSON' ) {
|
||||
$this->body_processor = $processor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a string to all lowercase and replace spaces and underscores with dashes.
|
||||
*
|
||||
* @param string $name Name.
|
||||
* @return string
|
||||
*/
|
||||
public function normalize_header_name( $name ) {
|
||||
return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get match-able values from a collection of targets.
|
||||
*
|
||||
* This function expects an associative array of target items, and returns an array of possible values from those targets that can be used to match against.
|
||||
* The key is the lowercase target name (i.e. `args`, `request_headers`, etc) - see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v3.x)#Variables
|
||||
* The value is an associative array of options that define how to narrow down the returned values for that target if it's an array (ARGS, for example). The possible options are:
|
||||
* count: If `true`, then the returned value will a count of how many matched targets were found, rather then the actual values of those targets.
|
||||
* For example, &ARGS_GET will return the number of keys the query string.
|
||||
* only: If specified, then only values in that target that match the given key will be returned.
|
||||
* For example, ARGS_GET:id|ARGS_GET:/^name/ will only return the values for `$_GET['id']` and any key in `$_GET` that starts with `name`
|
||||
* except: If specified, then values in that target will be left out from the returned values (even if they were included in an `only` option)
|
||||
* For example, ARGS_GET|!ARGS_GET:z will return every value from `$_GET` except for `$_GET['z']`.
|
||||
*
|
||||
* This function will return an array of associative arrays. Each with:
|
||||
* name: The target name that this value came from (i.e. the key in the input `$targets` argument )
|
||||
* source: For targets that are associative arrays (like ARGS), this will be the target name AND the key in that target (i.e. "args:z" for ARGS:z)
|
||||
* value: The value that was found in the associated target.
|
||||
*
|
||||
* @param TargetBag $targets An assoc. array with keys that are target name(s) and values are options for how to process that target (include/exclude rules, whether to return values or counts).
|
||||
* @return array{name: string, source: string, value: mixed}[]
|
||||
*/
|
||||
public function normalize_targets( $targets ) {
|
||||
$return = array();
|
||||
foreach ( $targets as $k => $v ) {
|
||||
$count_only = isset( $v['count'] ) ? self::NORMALIZE_ARRAY_COUNT : 0;
|
||||
$only = $v['only'] ?? array();
|
||||
$except = $v['except'] ?? array();
|
||||
$_k = strtolower( $k );
|
||||
switch ( $_k ) {
|
||||
case 'request_headers':
|
||||
$this->normalize_array_target(
|
||||
// get the headers that came in with this request
|
||||
$this->meta( 'headers' ),
|
||||
// ensure only and exclude filters are normalized
|
||||
array_map( array( $this->request, 'normalize_header_name' ), $only ),
|
||||
array_map( array( $this->request, 'normalize_header_name' ), $except ),
|
||||
$k,
|
||||
$return,
|
||||
// flags
|
||||
$count_only
|
||||
);
|
||||
continue 2;
|
||||
case 'request_headers_names':
|
||||
$this->normalize_array_target( $this->meta( 'headers_names' ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
|
||||
continue 2;
|
||||
case 'request_method':
|
||||
case 'request_protocol':
|
||||
case 'request_uri':
|
||||
case 'request_uri_raw':
|
||||
case 'request_filename':
|
||||
case 'request_basename':
|
||||
case 'request_body':
|
||||
case 'query_string':
|
||||
case 'request_line':
|
||||
$v = $this->meta( $_k );
|
||||
break;
|
||||
case 'tx':
|
||||
case 'ip':
|
||||
$this->normalize_array_target( $this->state_values( "$k." ), $only, $except, $k, $return, $count_only );
|
||||
continue 2;
|
||||
case 'request_cookies':
|
||||
case 'args':
|
||||
case 'args_get':
|
||||
case 'args_post':
|
||||
case 'files':
|
||||
$this->normalize_array_target( $this->meta( $_k ), $only, $except, $k, $return, $count_only );
|
||||
continue 2;
|
||||
case 'request_cookies_names':
|
||||
case 'args_names':
|
||||
case 'args_get_names':
|
||||
case 'args_post_names':
|
||||
case 'files_names':
|
||||
// get the "full" data (for 'args_names' get data for 'args') and stripe it down to just the key names
|
||||
$data = array_map(
|
||||
function ( $item ) {
|
||||
return $item[0]; },
|
||||
$this->meta( substr( $_k, 0, -6 ) )
|
||||
);
|
||||
$this->normalize_array_target( $data, $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
|
||||
continue 2;
|
||||
case 'matched_var':
|
||||
$this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only );
|
||||
continue 2;
|
||||
|
||||
case 'matched_var_name':
|
||||
$this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
|
||||
continue 2;
|
||||
|
||||
case 'matched_vars':
|
||||
$this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only );
|
||||
continue 2;
|
||||
|
||||
case 'matched_vars_names':
|
||||
$this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
|
||||
continue 2;
|
||||
|
||||
default:
|
||||
var_dump( 'Unknown target', $k, $v );
|
||||
exit( 0 );
|
||||
}
|
||||
$return[] = array(
|
||||
'name' => $k,
|
||||
'value' => $v,
|
||||
'source' => $k,
|
||||
);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset matched vars after processing a rule.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reset_matched_vars() {
|
||||
$this->matched_vars = array();
|
||||
$this->matched_vars_names = array();
|
||||
$this->matched_var = '';
|
||||
$this->matched_var_name = '';
|
||||
unset(
|
||||
$this->metadata['matched_var'],
|
||||
$this->metadata['matched_vars'],
|
||||
$this->metadata['matched_vars_names'],
|
||||
$this->metadata['matched_var_name']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the IP from the current request is in an array.
|
||||
*
|
||||
* @param array $array Array of IP addresses to verify the request IP against.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_ip_in_array( $array ) {
|
||||
$real_ip = $this->request->get_real_user_ip_address();
|
||||
$array_length = count( $array );
|
||||
|
||||
for ( $i = 0; $i < $array_length; $i++ ) {
|
||||
// Check if the IP matches a provided range or CIDR notation.
|
||||
$range = strpos( $array[ $i ], '/' ) !== false ? array( $array[ $i ], null ) : explode( '-', $array[ $i ] );
|
||||
if ( count( $range ) === 2 ) {
|
||||
if ( IP_Utils::ip_address_is_in_range( $real_ip, $range[0], $range[1] ) ) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the IP is an exact match.
|
||||
if ( $real_ip === $array[ $i ] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract values from an associative array, potentially applying filters and/or counting results.
|
||||
*
|
||||
* @param array{0: string, 1: scalar}|scalar[] $source The source assoc. array of values (i.e. $_GET, $_SERVER, etc.).
|
||||
* @param string[] $only Only include the values for these keys in the output.
|
||||
* @param string[] $excl Never include the values for these keys in the output.
|
||||
* @param string $name The name of this target (see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v3.x)#Variables).
|
||||
* @param array $results Array to add output values to, will be modified by this method.
|
||||
* @param int $flags Any of the NORMALIZE_ARRAY_* constants defined at the top of the class.
|
||||
*/
|
||||
private function normalize_array_target( $source, $only, $excl, $name, &$results, $flags = 0 ) {
|
||||
$output = array();
|
||||
$has_only = isset( $only[0] );
|
||||
$has_excl = isset( $excl[0] );
|
||||
|
||||
foreach ( $source as $source_key => $source_val ) {
|
||||
if ( is_array( $source_val ) ) {
|
||||
// if $source_val looks like a tuple from flatten_array(), then use the tuple as the key and value
|
||||
$source_key = $source_val[0];
|
||||
$source_val = $source_val[1];
|
||||
}
|
||||
$filter_match = ( $flags & self::NORMALIZE_ARRAY_MATCH_VALUES ) > 0 ? $source_val : $source_key;
|
||||
// if this key is on the "exclude" list, skip it
|
||||
if ( $has_excl && $this->key_matches( $filter_match, $excl ) ) {
|
||||
continue;
|
||||
}
|
||||
// if this key isn't in our "only" list, then skip it
|
||||
if ( $has_only && ! $this->key_matches( $filter_match, $only ) ) {
|
||||
continue;
|
||||
}
|
||||
// otherwise add this key/value to our output
|
||||
$output[] = array( $source_key, $source_val );
|
||||
}
|
||||
|
||||
if ( ( $flags & self::NORMALIZE_ARRAY_COUNT ) > 0 ) {
|
||||
// If we've been told to just count the values, then just count them.
|
||||
$results[] = array(
|
||||
'name' => (string) $name,
|
||||
'value' => count( $output ),
|
||||
'source' => '&' . $name,
|
||||
);
|
||||
} else {
|
||||
foreach ( $output as list( $item_name, $item_value ) ) {
|
||||
$results[] = array(
|
||||
'name' => (string) $item_name,
|
||||
'value' => $item_value,
|
||||
'source' => "$name:$item_name",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of tuples - probably from flatten_array() - return a new array
|
||||
* consisting of only the first value (the key name) from each tuple.
|
||||
*
|
||||
* @param array{0:string, 1:scalar}[] $flat_array An array of tuples.
|
||||
* @return string[]
|
||||
*/
|
||||
private function args_names( $flat_array ) {
|
||||
$names = array_map(
|
||||
function ( $tuple ) {
|
||||
return $tuple[0];
|
||||
},
|
||||
$flat_array
|
||||
);
|
||||
return array_unique( $names );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not a given $input key matches one of the given $patterns.
|
||||
*
|
||||
* @param string $input Key name to test against patterns.
|
||||
* @param string[] $patterns Patterns to test key name with.
|
||||
* @return bool
|
||||
*/
|
||||
private function key_matches( $input, $patterns ) {
|
||||
foreach ( $patterns as $p ) {
|
||||
if ( '/' === $p[0] ) {
|
||||
if ( 1 === preg_match( $p, $input ) ) {
|
||||
return true;
|
||||
}
|
||||
} elseif ( 0 === strcasecmp( $p, $input ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize output generated from the request that was blocked.
|
||||
*
|
||||
* @param string $output Output to sanitize.
|
||||
*/
|
||||
public function sanitize_output( $output ) {
|
||||
$url_decoded_output = rawurldecode( $output );
|
||||
$html_entities_output = htmlentities( $url_decoded_output, ENT_QUOTES, 'UTF-8' );
|
||||
// @phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
|
||||
$escapers = array( "\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c" );
|
||||
$replacements = array( "\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b" );
|
||||
// @phpcs:enable Squiz.Strings.DoubleQuoteUsage.NotRequired
|
||||
|
||||
return( str_replace( $escapers, $replacements, $html_entities_output ) );
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles generation and deletion of the bootstrap for the standalone WAF mode.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
|
||||
/**
|
||||
* Handles the bootstrap.
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class Waf_Standalone_Bootstrap {
|
||||
|
||||
/**
|
||||
* Ensures that constants are initialized if this class is used.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->guard_against_missing_abspath();
|
||||
$this->initialize_constants();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that this class is not used unless we are in the right context.
|
||||
*
|
||||
* @throws Waf_Exception If we are outside of WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function guard_against_missing_abspath() {
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
throw new Waf_Exception( 'Cannot generate the WAF bootstrap if we are not running in WordPress context.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the constants required for generating the bootstrap, if they have not been initialized yet.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function initialize_constants() {
|
||||
Waf_Constants::initialize_constants();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialized the WP filesystem and serves as a mocking hook for tests.
|
||||
*
|
||||
* Should only be implemented after the wp_loaded action hook:
|
||||
*
|
||||
* @link https://developer.wordpress.org/reference/functions/wp_filesystem/#more-information
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize_filesystem() {
|
||||
if ( ! function_exists( '\\WP_Filesystem' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the autoloader, which can then be used to require the autoloader in the generated boostrap file.
|
||||
*
|
||||
* @throws Waf_Exception In case the autoloader file cannot be found.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function locate_autoloader_file() {
|
||||
global $jetpack_autoloader_loader;
|
||||
|
||||
$autoload_file = null;
|
||||
|
||||
// Try the Jetpack autoloader.
|
||||
if ( isset( $jetpack_autoloader_loader ) ) {
|
||||
$class_file = $jetpack_autoloader_loader->find_class_file( Waf_Runner::class );
|
||||
if ( $class_file ) {
|
||||
$autoload_file = dirname( $class_file, 5 ) . '/vendor/autoload.php';
|
||||
}
|
||||
}
|
||||
|
||||
// Try Composer's autoloader.
|
||||
if ( null === $autoload_file
|
||||
&& is_callable( array( InstalledVersions::class, 'getInstallPath' ) )
|
||||
&& InstalledVersions::isInstalled( 'automattic/jetpack-waf' )
|
||||
) {
|
||||
$package_file = InstalledVersions::getInstallPath( 'automattic/jetpack-waf' );
|
||||
if ( substr( $package_file, -23 ) === '/automattic/jetpack-waf' ) {
|
||||
$autoload_file = dirname( $package_file, 3 ) . '/vendor/autoload.php';
|
||||
}
|
||||
}
|
||||
|
||||
// Guess. First look for being in a `vendor/automattic/jetpack-waf/src/', then see if we're standalone with our own vendor dir.
|
||||
if ( null === $autoload_file ) {
|
||||
$autoload_file = dirname( __DIR__, 4 ) . '/vendor/autoload.php';
|
||||
if ( ! file_exists( $autoload_file ) ) {
|
||||
$autoload_file = dirname( __DIR__ ) . '/vendor/autoload.php';
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the determined file actually exists.
|
||||
if ( ! file_exists( $autoload_file ) ) {
|
||||
throw new Waf_Exception( 'Cannot find autoloader, and the WAF standalone boostrap will not work without it.' );
|
||||
}
|
||||
|
||||
return $autoload_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the bootstrap.php file.
|
||||
*
|
||||
* @return string The bootstrap.php file path.
|
||||
*/
|
||||
public function get_bootstrap_file_path() {
|
||||
return trailingslashit( JETPACK_WAF_DIR ) . 'bootstrap.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entrypoint file.
|
||||
*
|
||||
* @return string The entrypoint file.
|
||||
*/
|
||||
private function get_entrypoint() {
|
||||
return defined( 'JETPACK_WAF_ENTRYPOINT' ) ? JETPACK_WAF_ENTRYPOINT : 'rules/rules.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the bootstrap file.
|
||||
*
|
||||
* @throws File_System_Exception If the filesystem is not available.
|
||||
* @throws File_System_Exception If the WAF directory cannot be created.
|
||||
* @throws File_System_Exception If the bootstrap file cannot be created.
|
||||
*
|
||||
* @return string Absolute path to the bootstrap file.
|
||||
*/
|
||||
public function generate() {
|
||||
|
||||
$this->initialize_filesystem();
|
||||
|
||||
global $wp_filesystem;
|
||||
if ( ! $wp_filesystem ) {
|
||||
throw new File_System_Exception( 'Cannot work without the file system being initialized.' );
|
||||
}
|
||||
|
||||
$autoloader_file = $this->locate_autoloader_file();
|
||||
|
||||
$bootstrap_file = $this->get_bootstrap_file_path();
|
||||
$entrypoint = $this->get_entrypoint();
|
||||
$mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false );
|
||||
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
|
||||
$share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
|
||||
|
||||
$code = "<?php\n"
|
||||
. sprintf( "define( 'DISABLE_JETPACK_WAF', %s );\n", var_export( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF, true ) )
|
||||
. "if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) return;\n"
|
||||
. sprintf( "define( 'JETPACK_WAF_MODE', %s );\n", var_export( $mode_option ? $mode_option : 'silent', true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_SHARE_DATA', %s );\n", var_export( $share_data_option, true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_SHARE_DEBUG_DATA', %s );\n", var_export( $share_debug_data_option, true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_DIR', %s );\n", var_export( JETPACK_WAF_DIR, true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_WPCONFIG', %s );\n", var_export( JETPACK_WAF_WPCONFIG, true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_ENTRYPOINT', %s );\n", var_export( $entrypoint, true ) )
|
||||
. 'require_once ' . var_export( $autoloader_file, true ) . ";\n"
|
||||
. "Automattic\Jetpack\Waf\Waf_Runner::initialize();\n";
|
||||
|
||||
if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) {
|
||||
if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) {
|
||||
throw new File_System_Exception( 'Failed creating WAF standalone bootstrap file directory: ' . JETPACK_WAF_DIR );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $wp_filesystem->put_contents( $bootstrap_file, $code ) ) {
|
||||
throw new File_System_Exception( 'Failed writing WAF standalone bootstrap file to: ' . $bootstrap_file );
|
||||
}
|
||||
|
||||
return $bootstrap_file;
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* Class used to retrieve WAF stats
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Automattic\Jetpack\IP\Utils as IP_Utils;
|
||||
|
||||
/**
|
||||
* Retrieves WAF stats.
|
||||
*/
|
||||
class Waf_Stats {
|
||||
|
||||
/**
|
||||
* Retrieve blocked requests from database
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_blocked_requests() {
|
||||
return array(
|
||||
'current_day' => Waf_Blocklog_Manager::get_current_day_block_count(),
|
||||
'thirty_days' => Waf_Blocklog_Manager::get_thirty_days_block_counts(),
|
||||
'all_time' => Waf_Blocklog_Manager::get_all_time_block_count(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IP allow list count
|
||||
*
|
||||
* @return int The number of valid IP addresses in the allow list
|
||||
*
|
||||
* @deprecated 0.20.1 Use Automattic\Jetpack\Waf\Waf_Blocklog_Manager API instead.
|
||||
*/
|
||||
public static function get_ip_allow_list_count() {
|
||||
_deprecated_function( __METHOD__, 'waf-0.20.1', 'Automattic\Jetpack\Waf\Waf_Blocklog_Manager' );
|
||||
|
||||
$ip_allow_list = get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME );
|
||||
|
||||
if ( ! $ip_allow_list ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$results = IP_Utils::get_ip_addresses_from_string( $ip_allow_list );
|
||||
|
||||
return count( $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IP block list count
|
||||
*
|
||||
* @return int The number of valid IP addresses in the block list
|
||||
*
|
||||
* @deprecated 0.20.1 Use Automattic\Jetpack\Waf\Waf_Blocklog_Manager API instead.
|
||||
*/
|
||||
public static function get_ip_block_list_count() {
|
||||
_deprecated_function( __METHOD__, 'waf-0.20.1', 'Automattic\Jetpack\Waf\Waf_Blocklog_Manager' );
|
||||
|
||||
$ip_block_list = get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME );
|
||||
|
||||
if ( ! $ip_block_list ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$results = IP_Utils::get_ip_addresses_from_string( $ip_block_list );
|
||||
|
||||
return count( $results );
|
||||
}
|
||||
|
||||
/** The global stats cache
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
public static $global_stats = null;
|
||||
|
||||
/**
|
||||
* Get Rules version
|
||||
*
|
||||
* @return bool|string False if value is not found. The current stored rules version if cache is found.
|
||||
*
|
||||
* @deprecated 0.12.3 Use Automattic\Jetpack\Waf\Waf_Stats::get_automatic_rules_last_updated() to version the rules instead.
|
||||
*/
|
||||
public static function get_rules_version() {
|
||||
_deprecated_function( __METHOD__, 'waf-0.12.3', 'Automattic\Jetpack\Waf\Waf_Stats::get_automatic_rules_last_updated' );
|
||||
|
||||
return get_option( Waf_Rules_Manager::VERSION_OPTION_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Automatic Rules last updated timestamp
|
||||
*
|
||||
* @return bool|string False if value is not found. The timestamp the current stored rules was last updated if cache is found.
|
||||
*/
|
||||
public static function get_automatic_rules_last_updated() {
|
||||
return get_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current cached global stats is expired and should be renewed
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_global_stats_cache_expired() {
|
||||
$option_timestamp = get_option( 'jetpack_protect_global_stats_timestamp' );
|
||||
|
||||
if ( ! $option_timestamp ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return time() > (int) $option_timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should consider the stored cache or bypass it
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function should_use_global_stats_cache() {
|
||||
return ! ( defined( 'JETPACK_PROTECT_DEV__BYPASS_CACHE' ) && JETPACK_PROTECT_DEV__BYPASS_CACHE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global stats from the API endpoint
|
||||
*/
|
||||
public static function fetch_global_stats_from_api() {
|
||||
$url = esc_url_raw( 'https://public-api.wordpress.com/wpcom/v2/jetpack-protect-global-stats' );
|
||||
$response = wp_remote_get( $url );
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) {
|
||||
return new \WP_Error( 'failed_fetching_global_stats', 'Failed to fetch global stats data from the server', array( 'status' => $response_code ) );
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
update_option( 'jetpack_protect_global_stats', maybe_serialize( $body ) );
|
||||
update_option( 'jetpack_protect_global_stats_timestamp', time() + 86400 ); // Caches expires after 24 hours.
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current cached global stats
|
||||
*
|
||||
* @return bool|array False if value is not found. Array with values if cache is found.
|
||||
*/
|
||||
public static function get_global_stats_from_options() {
|
||||
return maybe_unserialize( get_option( 'jetpack_protect_global_stats' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global stats
|
||||
* If the cache is expired, it will fetch the data from the API
|
||||
* If the cache is not expired, it will return the cached data
|
||||
*
|
||||
* @param bool $refresh_from_wpcom Whether to refresh the data from the API.
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public static function get_global_stats( $refresh_from_wpcom = false ) {
|
||||
if ( self::$global_stats !== null ) {
|
||||
return self::$global_stats;
|
||||
}
|
||||
|
||||
if ( $refresh_from_wpcom || ! self::should_use_global_stats_cache() || self::is_global_stats_cache_expired() ) {
|
||||
$global_stats = self::fetch_global_stats_from_api();
|
||||
} else {
|
||||
$global_stats = self::get_global_stats_from_options();
|
||||
}
|
||||
|
||||
// Ensure that $global_stats is of the correct type
|
||||
if ( ( ! is_object( $global_stats ) && ! ( $global_stats instanceof \WP_Error ) ) ) {
|
||||
return new \WP_Error( 'unexpected_type', 'Unexpected type or null returned for global stats' );
|
||||
}
|
||||
|
||||
return $global_stats;
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
<?php
|
||||
/**
|
||||
* Transforms for Jetpack Waf
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
/**
|
||||
* Waf_Transforms class
|
||||
*/
|
||||
class Waf_Transforms {
|
||||
|
||||
/**
|
||||
* Decode a Base64-encoded string. This runs the decode without strict mode, to match Modsecurity's 'base64DecodeExt' transform function.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function base64_decode_ext( $value ) {
|
||||
return base64_decode( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Characters to match when trimming a string.
|
||||
* Emulates `std::isspace` used by ModSecurity.
|
||||
*
|
||||
* @see https://en.cppreference.com/w/cpp/string/byte/isspace
|
||||
*/
|
||||
const TRIM_CHARS = " \n\r\t\v\f";
|
||||
|
||||
/**
|
||||
* Decode a Base64-encoded string. This runs the decode with strict mode, to match Modsecurity's 'base64Decode' transform function.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function base64_decode( $value ) {
|
||||
return base64_decode( $value, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all characters that might escape a command line command
|
||||
*
|
||||
* @see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-%28v2.x%29#cmdLine
|
||||
* @param string $value value to be escaped.
|
||||
* @return string
|
||||
*/
|
||||
public function cmd_line( $value ) {
|
||||
return strtolower(
|
||||
preg_replace(
|
||||
'/\s+/',
|
||||
' ',
|
||||
str_replace(
|
||||
array( ',', ';' ),
|
||||
' ',
|
||||
preg_replace(
|
||||
'/\s+(?=[\/\(])/',
|
||||
'',
|
||||
str_replace(
|
||||
array( '^', "'", '"', '\\' ),
|
||||
'',
|
||||
$value
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a SQL hex string.
|
||||
*
|
||||
* @example 414243 decodes to "ABC"
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function sql_hex_decode( $value ) {
|
||||
return preg_replace_callback(
|
||||
'/0x[a-f0-9]+/i',
|
||||
function ( $matches ) {
|
||||
$str = substr( $matches[0], 2 );
|
||||
if ( 0 !== strlen( $str ) % 2 ) {
|
||||
$str = '0' . $str;
|
||||
}
|
||||
return hex2bin( $str );
|
||||
},
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string using Base64 encoding.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function base64_encode( $value ) {
|
||||
return base64_encode( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all whitespace characters to a space and remove any repeated spaces.
|
||||
*
|
||||
* @param string $value value to be converted.
|
||||
* @return string
|
||||
*/
|
||||
public function compress_whitespace( $value ) {
|
||||
return preg_replace( '/\s+/', ' ', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode string (possibly containing binary characters) by replacing each input byte with two hexadecimal characters.
|
||||
*
|
||||
* @param string $value value to be encoded.
|
||||
* @return string
|
||||
*/
|
||||
public function hex_encode( $value ) {
|
||||
return bin2hex( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode string that was previously encoded by hexEncode()
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function hex_decode( $value ) {
|
||||
return pack( 'H*', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the characters encoded as HTML entities.
|
||||
*
|
||||
* @param mixed $value value do be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function html_entity_decode( $value ) {
|
||||
return html_entity_decode( $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the length of the input string.
|
||||
*
|
||||
* @param string $value input string.
|
||||
* @return int
|
||||
*/
|
||||
public function length( $value ) {
|
||||
return strlen( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all characters to lowercase.
|
||||
*
|
||||
* @param string $value string to be converted.
|
||||
* @return string
|
||||
*/
|
||||
public function lowercase( $value ) {
|
||||
return strtolower( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an md5 hash for the given data
|
||||
*
|
||||
* @param mixed $value value to be hashed.
|
||||
* @return string
|
||||
*/
|
||||
public function md5( $value ) {
|
||||
return md5( $value, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple slashes, directory self-references, and directory back-references (except when at the beginning of the input) from input string.
|
||||
*
|
||||
* @param string $value value to be normalized.
|
||||
* @return string
|
||||
*/
|
||||
public function normalize_path( $value ) {
|
||||
$parts = explode(
|
||||
'/',
|
||||
// replace any duplicate slashes with a single one.
|
||||
preg_replace( '~/{2,}~', '/', $value )
|
||||
);
|
||||
|
||||
$i = 0;
|
||||
while ( isset( $parts[ $i ] ) ) {
|
||||
switch ( $parts[ $i ] ) {
|
||||
// If this folder is a self-reference, remove it.
|
||||
case '..':
|
||||
// If this folder is a backreference, remove it unless we're already at the root.
|
||||
if ( isset( $parts[ $i - 1 ] ) && ! in_array( $parts[ $i - 1 ], array( '', '..' ), true ) ) {
|
||||
array_splice( $parts, $i - 1, 2 );
|
||||
--$i;
|
||||
continue 2;
|
||||
}
|
||||
break;
|
||||
case '.':
|
||||
array_splice( $parts, $i, 1 );
|
||||
continue 2;
|
||||
}
|
||||
++$i;
|
||||
}
|
||||
|
||||
return implode( '/', $parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert backslash characters to forward slashes, and then normalize using `normalizePath`
|
||||
*
|
||||
* @param string $value to be normalized.
|
||||
* @return string
|
||||
*/
|
||||
public function normalize_path_win( $value ) {
|
||||
return $this->normalize_path( str_replace( '\\', '/', $value ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all NUL bytes from input.
|
||||
*
|
||||
* @param string $value value to be filtered.
|
||||
* @return string
|
||||
*/
|
||||
public function remove_nulls( $value ) {
|
||||
return str_replace( "\x0", '', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all whitespace characters from input.
|
||||
*
|
||||
* @param string $value value to be filtered.
|
||||
* @return string
|
||||
*/
|
||||
public function remove_whitespace( $value ) {
|
||||
return preg_replace( '/\s/', '', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces each occurrence of a C-style comment (/ * ... * /) with a single space.
|
||||
* Unterminated comments will also be replaced with a space. However, a standalone termination of a comment (* /) will not be acted upon.
|
||||
*
|
||||
* @param string $value value to be filtered.
|
||||
* @return string
|
||||
*/
|
||||
public function replace_comments( $value ) {
|
||||
$value = preg_replace( '~/\*.*?\*/|/\*.*?$~Ds', ' ', $value );
|
||||
return explode( '/*', $value, 2 )[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes common comments chars (/ *, * /, --, #).
|
||||
*
|
||||
* @param string $value value to be filtered.
|
||||
* @return string
|
||||
*/
|
||||
public function remove_comments_char( $value ) {
|
||||
return preg_replace( '~/*|*/|--|#|//~', '', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces each NUL byte in input with a space.
|
||||
*
|
||||
* @param string $value value to be filtered.
|
||||
* @return string
|
||||
*/
|
||||
public function replace_nulls( $value ) {
|
||||
return str_replace( "\x0", ' ', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a URL-encoded input string.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function url_decode( $value ) {
|
||||
return urldecode( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a URL-encoded input string.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function url_decode_uni( $value ) {
|
||||
error_log( 'JETPACKWAF TRANSFORM NOT IMPLEMENTED: urlDecodeUni' );
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a json encoded input string.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function js_decode( $value ) {
|
||||
error_log( 'JETPACKWAF TRANSFORM NOT IMPLEMENTED: jsDecode' );
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all characters to uppercase.
|
||||
*
|
||||
* @param string $value value to be encoded.
|
||||
* @return string
|
||||
*/
|
||||
public function uppercase( $value ) {
|
||||
return strtoupper( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a SHA1 hash from the input string.
|
||||
*
|
||||
* @param mixed $value value to be hashed.
|
||||
* @return string
|
||||
*/
|
||||
public function sha1( $value ) {
|
||||
return sha1( $value, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove whitespace from the left side of the input string.
|
||||
*
|
||||
* @param string $value value to be trimmed.
|
||||
* @return string
|
||||
*/
|
||||
public function trim_left( $value ) {
|
||||
return ltrim( $value, self::TRIM_CHARS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove whitespace from the right side of the input string.
|
||||
*
|
||||
* @param string $value value to be trimmed.
|
||||
* @return string
|
||||
*/
|
||||
public function trim_right( $value ) {
|
||||
return rtrim( $value, self::TRIM_CHARS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove whitespace from both sides of the input string.
|
||||
*
|
||||
* @param string $value value to be trimmed.
|
||||
* @return string
|
||||
*/
|
||||
public function trim( $value ) {
|
||||
return trim( $value, self::TRIM_CHARS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UTF-8 characters to Unicode characters.
|
||||
*
|
||||
* This function iterates through each character of the input string, checks the ASCII value,
|
||||
* and converts it to its corresponding Unicode representation. It handles characters that are
|
||||
* represented with 1 to 4 bytes in UTF-8.
|
||||
*
|
||||
* @param string $str The string value to be encoded from UTF-8 to Unicode.
|
||||
* @return string The converted string with Unicode representation.
|
||||
*/
|
||||
public function utf8_to_unicode( $str ) {
|
||||
$unicodeStr = '';
|
||||
$strLen = strlen( $str );
|
||||
$i = 0;
|
||||
|
||||
// Iterate through each character of the input string.
|
||||
while ( $i < $strLen ) {
|
||||
// Get the ASCII value of the current character.
|
||||
$value = ord( $str[ $i ] );
|
||||
|
||||
if ( $value < 128 ) {
|
||||
// If the character is in the ASCII range (0-127), directly add it to the Unicode string.
|
||||
$unicodeStr .= chr( $value );
|
||||
++$i;
|
||||
} else {
|
||||
// For characters outside the ASCII range, determine the number of bytes in the UTF-8 representation.
|
||||
$unicodeValue = '';
|
||||
if ( $value >= 192 && $value <= 223 ) {
|
||||
// For characters represented with 2 bytes in UTF-8.
|
||||
$unicodeValue = ( ord( $str[ $i ] ) & 0x1F ) << 6 | ( ord( $str[ $i + 1 ] ) & 0x3F );
|
||||
$i += 2;
|
||||
} elseif ( $value >= 224 && $value <= 239 ) {
|
||||
// For characters represented with 3 bytes in UTF-8.
|
||||
$unicodeValue = ( ord( $str[ $i ] ) & 0x0F ) << 12 | ( ord( $str[ $i + 1 ] ) & 0x3F ) << 6 | ( ord( $str[ $i + 2 ] ) & 0x3F );
|
||||
$i += 3;
|
||||
} elseif ( $value >= 240 && $value <= 247 ) {
|
||||
// For characters represented with 4 bytes in UTF-8.
|
||||
$unicodeValue = ( ord( $str[ $i ] ) & 0x07 ) << 18 | ( ord( $str[ $i + 1 ] ) & 0x3F ) << 12 | ( ord( $str[ $i + 2 ] ) & 0x3F ) << 6 | ( ord( $str[ $i + 3 ] ) & 0x3F );
|
||||
$i += 4;
|
||||
} else {
|
||||
// If the sequence does not match any known UTF-8 pattern, skip to the next character.
|
||||
++$i;
|
||||
continue;
|
||||
}
|
||||
// Convert the Unicode value to a formatted string and append it to the Unicode string.
|
||||
$unicodeStr .= sprintf( '%%u%04X', $unicodeValue );
|
||||
}
|
||||
}
|
||||
|
||||
return strtolower( $unicodeStr );
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* File system exception.
|
||||
*
|
||||
* @since 0.10.1
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom exception for WAF file system errors.
|
||||
*/
|
||||
class File_System_Exception extends Waf_Exception {
|
||||
|
||||
/**
|
||||
* Error slug which maps to WP_Error::$code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SLUG = 'file_system_error';
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Rules API exception.
|
||||
*
|
||||
* @since 0.10.1
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom exception for WAF rules API errors.
|
||||
*/
|
||||
class Rules_API_Exception extends Waf_Exception {
|
||||
|
||||
/**
|
||||
* Error slug which maps to WP_Error::$code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SLUG = 'rules_api_error';
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Base custom exception for the WAF package.
|
||||
*
|
||||
* @since 0.10.1
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* WAF exception.
|
||||
*/
|
||||
class Waf_Exception extends Exception {
|
||||
|
||||
/**
|
||||
* Error slug which maps to WP_Error::$code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SLUG = 'waf_error';
|
||||
|
||||
/**
|
||||
* Convert the exception into a WP_Error object.
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function get_wp_error() {
|
||||
return new WP_Error( static::SLUG, $this->getMessage() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* Utility functions for WAF.
|
||||
*
|
||||
* @package automattic/jetpack-waf
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Waf;
|
||||
|
||||
/**
|
||||
* A wrapper for WordPress's `wp_unslash()`.
|
||||
*
|
||||
* Even though PHP itself dropped the option to add slashes to superglobals a decade ago,
|
||||
* WordPress still does it through some misguided extreme backwards compatibility. 🙄
|
||||
*
|
||||
* If WordPress's function exists, assume it needs to be called. If not, assume it doesn't.
|
||||
*
|
||||
* @param string|array $value String or array of data to unslash.
|
||||
* @return string|array Possibly unslashed $value.
|
||||
*/
|
||||
function wp_unslash( $value ) {
|
||||
if ( function_exists( '\\wp_unslash' ) ) {
|
||||
return \wp_unslash( $value );
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP helpfully parses request data into nested arrays in superglobals like $_GET and $_POST,
|
||||
* and as part of that parsing turns field names like "myfield[x][y]" into a nested array
|
||||
* that looks like [ "myfield" => [ "x" => [ "y" => "..." ] ] ]
|
||||
* However, modsecurity (and thus our WAF rules) expect the original (non-nested) names.
|
||||
*
|
||||
* Therefore, this method takes an array of any depth and returns a single-depth array with nested
|
||||
* keys translated back to a single string with brackets.
|
||||
*
|
||||
* Because there might be multiple items with the same name, this function will return an array of tuples,
|
||||
* with the first item in the tuple the re-created original field name, and the second item the value.
|
||||
*
|
||||
* @example
|
||||
* flatten_array( [ "field1" => "abc", "field2" => [ "d", "e", "f" ] ] )
|
||||
* => [
|
||||
* [ "field1", "abc" ],
|
||||
* [ "field2[0]", "d" ],
|
||||
* [ "field2[1]", "e" ],
|
||||
* [ "field2[2]", "f" ],
|
||||
* ]
|
||||
*
|
||||
* @param array $array An array that resembles one of the PHP superglobals like $_GET or $_POST.
|
||||
* @param string $key_prefix String that should be prepended to the keys output by this function.
|
||||
* Usually only used internally as part of recursion when flattening a nested array.
|
||||
* @param bool|null $dot_notation Whether to use dot notation instead of bracket notation.
|
||||
*
|
||||
* @return array{0: string, 1: scalar}[] $key_prefix An array of key/value tuples, one for each distinct value in the input array.
|
||||
*/
|
||||
function flatten_array( $array, $key_prefix = '', $dot_notation = null ) {
|
||||
$return = array();
|
||||
foreach ( $array as $source_key => $source_value ) {
|
||||
$key = $source_key;
|
||||
if ( ! empty( $key_prefix ) ) {
|
||||
$key = $dot_notation ? "$key_prefix.$source_key" : $key_prefix . "[$source_key]";
|
||||
}
|
||||
|
||||
if ( ! is_array( $source_value ) ) {
|
||||
$return[] = array( $key, $source_value );
|
||||
} else {
|
||||
$return = array_merge( $return, flatten_array( $source_value, $key, $dot_notation ) );
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for getallheaders, which is not available in all PHP environments.
|
||||
*
|
||||
* @link https://github.com/ralouphie/getallheaders
|
||||
*/
|
||||
if ( ! function_exists( 'getallheaders' ) ) {
|
||||
/**
|
||||
* Get all HTTP header key/values as an associative array for the current request.
|
||||
*
|
||||
* @return array The HTTP header key/value pairs.
|
||||
*/
|
||||
function getallheaders() {
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput
|
||||
$headers = array();
|
||||
|
||||
$copy_server = array(
|
||||
'CONTENT_TYPE' => 'Content-Type',
|
||||
'CONTENT_LENGTH' => 'Content-Length',
|
||||
'CONTENT_MD5' => 'Content-Md5',
|
||||
);
|
||||
|
||||
foreach ( $_SERVER as $key => $value ) {
|
||||
if ( substr( $key, 0, 5 ) === 'HTTP_' ) {
|
||||
$key = substr( $key, 5 );
|
||||
if ( ! isset( $copy_server[ $key ] ) || ! isset( $_SERVER[ $key ] ) ) {
|
||||
$key = str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', $key ) ) ) );
|
||||
$headers[ $key ] = $value;
|
||||
}
|
||||
} elseif ( isset( $copy_server[ $key ] ) ) {
|
||||
$headers[ $copy_server[ $key ] ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! isset( $headers['Authorization'] ) ) {
|
||||
if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
|
||||
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
|
||||
} elseif ( isset( $_SERVER['PHP_AUTH_USER'] ) ) {
|
||||
$basic_pass = $_SERVER['PHP_AUTH_PW'] ?? '';
|
||||
$headers['Authorization'] = 'Basic ' . base64_encode( $_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass );
|
||||
} elseif ( isset( $_SERVER['PHP_AUTH_DIGEST'] ) ) {
|
||||
$headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user