├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── composer.json ├── inc └── helpers.php ├── phpunit.xml.dist └── src ├── ArrayContext.php ├── NonceContextInterface.php ├── NonceInterface.php ├── RequestGlobalsContext.php └── WpNonce.php /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - hhvm 8 | 9 | matrix: 10 | allow_failures: 11 | - php: hhvm 12 | 13 | before_script: 14 | - composer install 15 | 16 | script: 17 | - mkdir -p .build/logs 18 | - vendor/bin/phpunit -c phpunit.xml.dist 19 | 20 | after_success: 21 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **Don't** use issue tracker (nor send any pull request) if you find a **security** issue. 2 | They are public, so please send an email to the address on my [Github profile](https://github.com/Giuseppe-Mazzapica) 3 | 4 | ---- 5 | 6 | Before work on features or bug fix you might want to open an issue first. 7 | 8 | No need to do this for small things or evident bugs that need a fix. 9 | 10 | After the change or new feature has been discussed, the contributing flow is: 11 | 12 | 1. Fork it 13 | 2. Create your feature or bug-fix branch 14 | 3. Make your changes 15 | 4. Commit your changes 16 | 5. Run the tests, adding new ones for your own code if necessary. 17 | 6. Repeat 4, 5 and 6 until all tests pass. 18 | 6. Push to the branch 19 | 7. Create a pull request from your branch to "dev" branch -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Giuseppe Mazzapica 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brain/nonces", 3 | "description": "OOP package for WordPress to deal with nonces.", 4 | "type": "package", 5 | "keywords": [ 6 | "wordpress", 7 | "nonce", 8 | "wordpress nonce", 9 | "security" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Giuseppe Mazzapica", 15 | "email": "giuseppe.mazzapica@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=5.5.0" 21 | }, 22 | "require-dev": { 23 | "brain/monkey": "~1.4", 24 | "phpunit/phpunit": "~4.8" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Brain\\Nonces\\": "src/" 29 | }, 30 | "files": [ 31 | "inc/helpers.php" 32 | ] 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Brain\\Nonces\\Tests\\": "tests/src/" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /inc/helpers.php: -------------------------------------------------------------------------------- 1 | ', 27 | esc_attr($nonce->action()), 28 | esc_attr((string)$nonce) 29 | ); 30 | } 31 | 32 | /** 33 | * Adds nonces action and value to a given URL. 34 | * 35 | * If URL is not provided, current URL is used. 36 | * 37 | * @param NonceInterface $nonce 38 | * @param string|null $url 39 | * @return string 40 | */ 41 | function nonceUrl(NonceInterface $nonce, $url = null) 42 | { 43 | if (!$url || !is_string($url)) { 44 | $home_path = trim(parse_url(home_url(), PHP_URL_PATH), '/'); 45 | $current_url_path = trim(add_query_arg([]), '/'); 46 | if ($home_path && strpos($current_url_path, $home_path) === 0) { 47 | $current_url_path = substr($current_url_path, strlen($home_path)); 48 | } 49 | $url = home_url(urldecode($current_url_path)); 50 | } 51 | 52 | return esc_url_raw(add_query_arg($nonce->action(), (string)$nonce, $url)); 53 | } 54 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | ./src 16 | ./inc 17 | 18 | ./tests 19 | ./vendor 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./tests/src/ 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/ArrayContext.php: -------------------------------------------------------------------------------- 1 | 18 | * @package Nonces 19 | * @license http://opensource.org/licenses/MIT MIT 20 | */ 21 | final class ArrayContext implements NonceContextInterface 22 | { 23 | 24 | private $storage = []; 25 | 26 | /** 27 | * @param array $storage 28 | */ 29 | public function __construct(array $storage) 30 | { 31 | $this->storage = $storage; 32 | } 33 | 34 | /** 35 | * @inheritdoc 36 | */ 37 | #[\ReturnTypeWillChange] 38 | public function offsetExists($offset) 39 | { 40 | return array_key_exists($offset, $this->storage); 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | #[\ReturnTypeWillChange] 47 | public function offsetGet($offset) 48 | { 49 | return $this->offsetExists($offset) ? $this->storage[$offset] : null; 50 | } 51 | 52 | /** 53 | * Disabled. 54 | * 55 | * @param mixed $offset 56 | * @param mixed $value 57 | * 58 | * @throws \BadMethodCallException 59 | */ 60 | #[\ReturnTypeWillChange] 61 | public function offsetSet($offset, $value) 62 | { 63 | throw new \BadMethodCallException( 64 | sprintf("Can't call %s, %s is read only.", __METHOD__, __CLASS__) 65 | ); 66 | } 67 | 68 | /** 69 | * Disabled. 70 | * 71 | * @param mixed $offset 72 | * 73 | * @throws \BadMethodCallException 74 | */ 75 | #[\ReturnTypeWillChange] 76 | public function offsetUnset($offset) 77 | { 78 | throw new \BadMethodCallException( 79 | sprintf("Can't call %s, %s is read only.", __METHOD__, __CLASS__) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/NonceContextInterface.php: -------------------------------------------------------------------------------- 1 | 17 | * @package Nonces 18 | * @license http://opensource.org/licenses/MIT MIT 19 | */ 20 | interface NonceContextInterface extends \ArrayAccess 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/NonceInterface.php: -------------------------------------------------------------------------------- 1 | 17 | * @package Brain\Nonces 18 | * @license http://opensource.org/licenses/MIT MIT 19 | */ 20 | interface NonceInterface 21 | { 22 | /** 23 | * Returns the nonce action as string. 24 | * 25 | * @return string 26 | */ 27 | public function action(); 28 | 29 | /** 30 | * Returns the nonce value as string. 31 | * 32 | * @return string 33 | */ 34 | public function __toString(); 35 | 36 | /** 37 | * Validates the nonce against an optionally given context. 38 | * 39 | * What to do in case of missing context is left to implementations. 40 | * 41 | * Custom implementation of context interface can provide different values to be used for 42 | * validation. 43 | * 44 | * @param NonceContextInterface $context 45 | * @return bool 46 | */ 47 | public function validate(NonceContextInterface $context = null); 48 | } 49 | -------------------------------------------------------------------------------- /src/RequestGlobalsContext.php: -------------------------------------------------------------------------------- 1 | 22 | * @package Nonces 23 | * @license http://opensource.org/licenses/MIT MIT 24 | */ 25 | final class RequestGlobalsContext implements NonceContextInterface 26 | { 27 | 28 | /** 29 | * @var ArrayContext context 30 | */ 31 | private $context; 32 | 33 | /** 34 | * We don't use `$_REQUEST` because, by default, in PHP it gives precedence to `$_GET` over 35 | * `$_POST` in POST requests, and being dependant on `request_order` / `variables_order` ini 36 | * configurations it is not consistent across systems. 37 | */ 38 | public function __construct() 39 | { 40 | $http_method = empty($_SERVER['REQUEST_METHOD']) ? null : $_SERVER['REQUEST_METHOD']; 41 | $is_post = is_string($http_method) && strtoupper($http_method) === 'POST'; 42 | $request = $is_post ? array_merge($_GET, $_POST) : $_REQUEST; 43 | 44 | $this->context = new ArrayContext($request); 45 | } 46 | 47 | /** 48 | * Delegates to encapsulated context. 49 | * 50 | * @param mixed $offset 51 | * @return bool 52 | */ 53 | #[\ReturnTypeWillChange] 54 | public function offsetExists($offset) 55 | { 56 | return $this->context->offsetExists($offset); 57 | } 58 | 59 | /** 60 | * Delegates to encapsulated context. 61 | * 62 | * @param mixed $offset 63 | * @return mixed 64 | */ 65 | #[\ReturnTypeWillChange] 66 | public function offsetGet($offset) 67 | { 68 | return $this->context->offsetGet($offset); 69 | } 70 | 71 | /** 72 | * Delegates to encapsulated context. 73 | * 74 | * @param mixed $offset 75 | * @param mixed $value 76 | */ 77 | #[\ReturnTypeWillChange] 78 | public function offsetSet($offset, $value) 79 | { 80 | $this->context->offsetSet($offset, $value); 81 | } 82 | 83 | /** 84 | * DDelegates to encapsulated context. 85 | * 86 | * @param mixed $offset 87 | */ 88 | #[\ReturnTypeWillChange] 89 | public function offsetUnset($offset) 90 | { 91 | $this->context->offsetUnset($offset); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/WpNonce.php: -------------------------------------------------------------------------------- 1 | 17 | * @package Brain\Nonces 18 | * @license http://opensource.org/licenses/MIT MIT 19 | */ 20 | final class WpNonce implements NonceInterface 21 | { 22 | 23 | /** 24 | * @var string 25 | */ 26 | private $action; 27 | 28 | /** 29 | * @var int 30 | */ 31 | private $life; 32 | 33 | /** 34 | * Constructor. Save properties as instance variables. 35 | * 36 | * We allow to customize nonce Time To Live, defaulting to 30 minutes (1800 seconds) that 37 | * is much less than the 24 hours of WordPress defaults. 38 | * 39 | * @param string $action 40 | * @param int $life 41 | */ 42 | public function __construct($action = '', $life = 1800) 43 | { 44 | $this->action = is_string($action) ? $action : ''; 45 | $this->life = is_numeric($life) ? (int)$life : 1800; 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | */ 51 | public function action() 52 | { 53 | return $this->action; 54 | } 55 | 56 | /** 57 | * Validates the nonce against given context. 58 | * 59 | * When not provided, context defaults to `RequestGlobalsContext`, so that value is searched 60 | * in super globals. 61 | * 62 | * We need to filter the nonce life and remove the filter afterwards, because WP does not 63 | * allow to filter nonce by action (yet? @link https://core.trac.wordpress.org/ticket/35188) 64 | * 65 | * @param NonceContextInterface $context 66 | * @return bool 67 | */ 68 | public function validate(NonceContextInterface $context = null) 69 | { 70 | $context or $context = new RequestGlobalsContext(); 71 | 72 | $value = $context->offsetExists($this->action) ? $context[$this->action] : ''; 73 | if (!$value || !is_string($value)) { 74 | return false; 75 | } 76 | 77 | $lifeFilter = $this->lifeFilter(); 78 | 79 | add_filter('nonce_life', $lifeFilter); 80 | $valid = wp_verify_nonce($value, $this->hashedAction()); 81 | remove_filter('nonce_life', $lifeFilter); 82 | 83 | return (bool)$valid; 84 | } 85 | 86 | /** 87 | * Returns the nonce string built with WordPress core function. 88 | * 89 | * We need to filter the nonce life and remove the filter afterwards, because WP does not 90 | * allow to filter nonce by action (yet? @link https://core.trac.wordpress.org/ticket/35188) 91 | * 92 | * @return string Nonce value. 93 | */ 94 | public function __toString() 95 | { 96 | $lifeFilter = $this->lifeFilter(); 97 | 98 | add_filter('nonce_life', $lifeFilter); 99 | $value = wp_create_nonce($this->hashedAction()); 100 | remove_filter('nonce_life', $lifeFilter); 101 | 102 | return $value; 103 | } 104 | 105 | /** 106 | * Returns the callback that will be used to filter nonce life. 107 | * 108 | * @return \Closure 109 | */ 110 | private function lifeFilter() 111 | { 112 | return function () { 113 | return $this->life; 114 | }; 115 | } 116 | 117 | /** 118 | * Returns an hashed version of the action. 119 | * 120 | * Current blog id is appended to nonce action to make nonce blog specific. 121 | * WordPress will hash the action, so we could avoid do the hashing here. 122 | * However, unlike WordPress, we don't have a nonce "key" in URL or form fields, we use the 123 | * action for that, so nonce action publicly clearly accessible. 124 | * For this reason we do hash to make sure that trace back the nonce value from the action 125 | * is as much hard as possible. 126 | * This relies on a strong salt, which is required anyway for good WP security. 127 | * 128 | * @return string 129 | */ 130 | private function hashedAction() 131 | { 132 | return wp_hash($this->action . get_current_blog_id(), 'nonce'); 133 | } 134 | } 135 | --------------------------------------------------------------------------------