A vulnerability has recently been disclosed that affects WordPress 2.8.3 and allows the admin user to be locked out of their account by changing the password.
This post on Full Disclosure details the flaw, and includes relevant code snippets. The post mentions that 'You can abuse the password reset function, and bypass the first step and then reset the admin password by submiting an array to the $key variable.'
I'd be interested in someone familiar with PHP explaining the bug in more detail.
Those affected should update to a new 2.8.4 release which apparently fixes the flaw.
wp-login.php:
...[snip]....
line 186:
function reset_password($key) {
global $wpdb;
$key = preg_replace('/[^a-z0-9]/i', '', $key);
if ( empty( $key ) )
return new WP_Error('invalid_key', __('Invalid key'));
$user = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->users WHERE
user_activation_key = %s", $key));
if ( empty( $user ) )
return new WP_Error('invalid_key', __('Invalid key'));
...[snip]....
line 276:
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : 'login';
$errors = new WP_Error();
if ( isset($_GET['key']) )
$action = 'resetpass';
// validate action so as to default to the login screen
if ( !in_array($action, array('logout', 'lostpassword', 'retrievepassword',
'resetpass', 'rp', 'register', 'login')) && false ===
has_filter('login_form_' . $action) )
$action = 'login';
...[snip]....
line 370:
break;
case 'resetpass' :
case 'rp' :
$errors = reset_password($_GET['key']);
if ( ! is_wp_error($errors) ) {
wp_redirect('wp-login.php?checkemail=newpass');
exit();
}
wp_redirect('wp-login.php?action=lostpassword&error=invalidkey');
exit();
break;
...[snip ]...
So $key is an array in the querystring with a single empty string ['']
http://DOMAIN_NAME.TLD/wp-login.php?action=rp&key[]=
reset_password gets called with an array, and then preg_replace gets called:
//$key = ['']
$key = preg_replace('/[^a-z0-9]/i', '', $key);
//$key = [''] still
because preg_replace accepts either a string or an array of strings. It regex replaces nothing and returns the same array. $key is not empty (it's an array of an empty string) so this happens:
$user = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->users
WHERE user_activation_key = %s", $key));
Now from here, I need to go read the wordpress source for how prepare behaves...
More:
So prepare calls vsprintf which produces an empty string
$a = array('');
$b = array($a);
vsprintf("%s", $b);
//Does not produce anything
So the SQL is:
SELECT * FROM $wpdb->users WHERE user_activation_key = ''
Which will apparently match the admin user (and all users without activation_keys I suppose).
And that's how.
I have a related question on how to patch this vulnerability - line 190 on the wp-login.php should now look like this;
if ( empty( $key ) || is_array( $key ) )
Related
I'm working with an aged wordpress theme which I really like and I only have some basic coding skills. My provider forcably upgraded my server php version to 7.2 and of course some of my scripts are breaking down.
public function localize( $handle, $object_name, $l10n ) {
if ( $handle === 'jquery' )
$handle = 'jquery-core';
if ( is_array($l10n) && isset($l10n['l10n_print_after']) ) { // back compat, preserve the code in 'l10n_print_after' if present
$after = $l10n['l10n_print_after'];
unset($l10n['l10n_print_after']);
}
foreach ( (array) $l10n as $key => $value ) {
if ( !is_scalar($value) )
continue;
$l10n[$key] = html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8');
}``
According to the log the error is in the last line because in that like apparently it "Cannot assign an empty string to a string offset"
Maybe this is a lot more complicated than changing one simple thing....any solutions for that?
The most logical thing to do here is to change it to update in the in_array condition:
if ( is_array($l10n){
if(isset($l10n['l10n_print_after']) ) { // back compat, preserve the code in 'l10n_print_after' if present
$after = $l10n['l10n_print_after'];
unset($l10n['l10n_print_after']);
}
foreach ($l10n as $key => $value ) {
if ( !is_scalar($value) )
continue;
$l10n[$key] = html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8');
}
}
Even if you cast (array)$l10n to an array, this doesn't set the variable itself to be an array ( like $l10n = (array)$l10n).
That said, working with mixed types can be really cumbersome. It's better to send it only arrays or deal with the array bit first, that way you have a consistent type. Like this:
public function localize( $handle, $object_name, $l10n ) {
//normalize arguments
if(!is_array($l10n)) $l10n = [$l10n];
if ( $handle === 'jquery' )
$handle = 'jquery-core';
if ( isset($l10n['l10n_print_after']) ) { // back compat, preserve the code in 'l10n_print_after' if present
$after = $l10n['l10n_print_after'];
unset($l10n['l10n_print_after']);
}
foreach ($l10n as $key => $value ) {
if ( !is_scalar($value) )
continue;
$l10n[$key] = html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8');
}
}
Another possible route to fixing this is to force your server to run an older version of php if they can do that. For example with pantheon.io you can force the server to run a certain version of php in the server config file.
Unless you would like to modernize all your scripts, then ignore this.
I have two URLs and am looking for the best way to decide if they are identical.
Example:
$url1 = 'http://example.com/page.php?tab=items&msg=3&sort=title';
$url2 = 'http://example.com/page.php?tab=items&sort=title&msg=3';
In the two URLs only the sort and msg param are switched, so I consider them equal.
However I cannot simply do if ( $url1 == $url2 ) { … }
I'm having a list of URLs and need to find duplicates, so the code should be fast as it is run inside a loop. (As a side note: The domain/page.php will always be same, it's only about finding URLs by params.)
Maybe like this?
function compare_url($url1, $url2){
return (parse_url($url1,PHP_URL_QUERY) == parse_url($url2,PHP_URL_QUERY));
}
It's not as easy as it might sound to find out if an URI is identical or not, especially as you take the query parameter into account here.
One common way to do this is to have a function that normalizes the URL and then compare the normalized URIs:
$url1 = 'http://example.com/page.php?tab=items&msg=3&sort=title';
$url2 = 'http://example.com/page.php?tab=items&sort=title&msg=3';
var_dump(url_nornalize($url1) == url_nornalize($url2)); # bool(true)
Into such a normalization function you put in your requirements. First of all the URL should be normalized according to the specs:
function url_nornalize($url, $separator = '&')
{
// normalize according RFC 3986
$url = new Net_URL2($url);
$url->normalize();
And then you can take care of additional normalization steps, for example, sorting the sub-parts of the query:
// normalize query if applicable
$query = $url->getQuery();
if (false !== $query) {
$params = explode($separator, $query);
sort($params);
$query = implode($separator, $params);
$url->setQuery($query);
}
Additional steps can be though of, like removing default parameters or not allowed ones, or duplicate ones and what not.
Finally the string of normalized URL is returned
return (string) $url;
}
Using an array/hash-map for the parameters isn't bad as well, I just wanted to show an alternative approach. Full example:
<?php
/**
* http://stackoverflow.com/questions/27667182/are-two-urls-identical-ignore-the-param-order
*/
require_once 'Net/URL2.php';
function url_nornalize($url, $separator = '&')
{
// normalize according RFC 3986
$url = new Net_URL2($url);
$url->normalize();
// normalize query if applicable
$query = $url->getQuery();
if (false !== $query) {
$params = explode($separator, $query);
// remove empty parameters
$params = array_filter($params, 'strlen');
// sort parameters
sort($params);
$query = implode($separator, $params);
$url->setQuery($query);
}
return (string)$url;
}
$url1 = 'http://EXAMPLE.com/p%61ge.php?tab=items&&&msg=3&sort=title';
$url2 = 'http://example.com:80/page.php?tab=items&sort=title&msg=3';
var_dump(url_nornalize($url1) == url_nornalize($url2)); # bool(true)
To make sure that both URLs are identical, we need to compare at least 4 elements:
The scheme(e.g. http, https, ftp)
The host, i.e. the domain name of the URL
The path, i.e. the "file" that was requested
Query parameters of the request.
Some notes:
(1) and (2) are case-insensitive, which means http://example.org is identical to HTTP://EXAMPLE.ORG.
(3) can have leading or trailing slashes, that should be ignored: example.org is identical to example.org/
(4) could include parameters in varying order.
We can safely ignore anchor text, or "fragment" (#anchor after the query parameters), as they are only parsed by the browser.
URLs can also include port-numbers, a username and password - I think we can ignore those elements, as they are used so rarely that they do not need to be checked here.
Solution:
Here's a complete function that checks all those details:
/**
* Check if two urls match while ignoring order of params
*
* #param string $url1
* #param string $url2
* #return bool
*/
function do_urls_match( $url1, $url2 ) {
// Parse urls
$parts1 = parse_url( $url1 );
$parts2 = parse_url( $url2 );
// Scheme and host are case-insensitive.
$scheme1 = strtolower( $parts1[ 'scheme' ] ?? '' );
$scheme2 = strtolower( $parts2[ 'scheme' ] ?? '' );
$host1 = strtolower( $parts1[ 'host' ] ?? '' );
$host2 = strtolower( $parts2[ 'host' ] ?? '' );
if ( $scheme1 !== $scheme2 ) {
// URL scheme mismatch (http <-> https): URLs are not identical.
return false;
}
if ( $host1 !== $host2 ) {
// Different host (domain name): Not identical.
return false;
}
// Remvoe leading/trailing slashes, url-decode special characters.
$path1 = trim( urldecode( $parts1[ 'path' ] ?? '' ), '/' );
$path2 = trim( urldecode( $parts2[ 'path' ] ?? '' ), '/' );
if ( $path1 !== $path2 ) {
// The request-path is different: Different URLs.
return false;
}
// Convert the query-params into arrays.
parse_str( $parts1['query'] ?? '', $query1 );
parse_str( $parts2['query'] ?? '', $query2 );
if ( count( $query1 ) !== count( $query2 ) ) {
// Both URLs have a differnt number of params: They cannot match.
return false;
}
// Only compare the query-arrays when params are present.
if (count( $query1 ) > 0 ) {
ksort( $query1 );
ksort( $query2 );
if ( array_diff( $query1, $query2 ) ) {
// Query arrays have differencs: URLs do not match.
return false;
}
}
// All checks passed, URLs are identical.
return true;
} // End do_urls_match()
Test cases:
$base_urls = [
'https://example.org/',
'https://example.org/index.php?sort=asc&field=id&filter=foo',
'http://EXAMPLE.com/p%61ge.php?tab=items&&&msg=3&sort=title',
];
$compare_urls = [
'https://example.org/',
'https://Example.Org',
'https://example.org/index.php?sort=asc&&field=id&filter=foo',
'http://example.org/index.php?sort=asc&field=id&filter=foo',
'https://company.net/page.php?sort=asc&field=id&filter=foo',
'https://example.org/index.php?sort=asc&&&field=id&filter=foo#anchor',
'https://example.org/index.php?field=id&filter=foo&sort=asc',
'http://example.com:80/page.php?tab=items&sort=title&msg=3',
];
foreach ( $base_urls as $url1 ) {
printf( "\n\n%s", $url1 );
foreach ( $compare_urls as $url2 ) {
if (do_urls_match( $url1, $url2 )) {
printf( "\n [MATCHES] %s", $url2 );
}
}
}
/* Output:
https://example.org/
[MATCHES] https://example.org/
[MATCHES] https://Example.Org
https://example.org/index.php?sort=asc&field=id&filter=foo
[MATCHES] https://example.org/index.php?sort=asc&&field=id&filter=foo
[MATCHES] https://example.org/index.php?sort=asc&&&field=id&filter=foo#anchor
[MATCHES] https://example.org/index.php?field=id&filter=foo&sort=asc
http://EXAMPLE.com/p%61ge.php?tab=items&&&msg=3&sort=title
[MATCHES] http://example.com:80/page.php?tab=items&sort=title&msg=3
*/
All the errors I'm interested in debugging in Codeigniter's log files are reporting that they come from /system/core/Loader.php when they don't. For example, here's a line:
ERROR | 2014-09-22 22:35:43 | "Severity: Notice --> Undefined variable: my_variable \/system\/core\/Loader.php(829) : eval()'d code 84"
I know which file this is coming from, and it's a view. I'm aware of debug_backtrace and I'm thinking about making a string of it and concatenating it onto the end of the $msg variable in an overridden Log.php, but I first wanted to check two things with all you friends:
Is debug_backtrace the best way to do this? It returns a huge amount of data.
Is anyone aware of someone that's already done this? Seems like an obvious need for anyone using Codeigniter (...still ;)
Here's some code you could adapt to your needs (I recommend creating a My_Log and not editing the system file just in case CI does ever get an update). The reason I have this is complicated and not useful as-is any longer, but it might give you a start:
protected function log_query ($query, $type)
{
$backtrace = debug_backtrace ();
$backtrace = array_slice ( $backtrace, 2, -2 ); // first and last two elements are things that never change
$traced = array ();
foreach ( $backtrace as $bt )
{
$func = isset ( $bt['function'] ) ? $bt['function'].'()' : '';
$line = isset ( $bt['line'] ) ? $bt['line'] : '';
$file = isset ( $bt['file'] ) ? str_replace ( APP, '', $bt['file'] ) : '';
$object = isset ( $bt['object'] ) ? get_class ( $bt['object'] ) : '';
$args = array (); build_args ( $bt['args'], $args );
$args = implode ( '; ', $args );
if ( $object )
{
$obj_func = $object . '->' . $func;
} else {
$obj_func = $func;
}
$traced[] = "$file $obj_func $line $args";
unset($args);
}
$traced = implode ( "\n\t", $traced );
$date = MYSQL_DATE_TIME;
$uri = isset ( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '' ;
$query= preg_replace ( "/[\n\t]/", ' ', $query );
log_message ('error', "{$query}\n\n\t{$date} ({$type})\n\t{$uri}\n\t{$traced}\n--------------------------\n");
}
PHP automatically creates arrays in $_GET, when the parameter name is followed by [] or [keyname].
However for a public API I'd love to have the same behaviour without explicit brackets in the URL. For example, the query
?foo=bar&foo=baz
should result in a $_GET (or similar) like this:
$_GET["foo"] == array("bar", "baz");
Is there any possibility to get this behaviour in PHP easily? I.e., not parsing $_SERVER['QUERY_STRING'] myself or preg_replacing = with []= in the query string before feeding it to parse_str()?
There's no built in way to support ?foo=bar&foo=baz.
Daniel Morell proposed a solution which manually parses the URL string and iteratively builds up an array when multiple instances of the parameter exist, or returns a string when only one parameter exists (ie; matches the default behaviour).
It supports both types of URLs, with and without a bracket:
?foo=bar&foo=baz // works
?foo[]=bar&foo[]=baz // works
/**
* Parses GET and POST form input like $_GET and $_POST, but without requiring multiple select inputs to end the name
* in a pair of brackets.
*
* #param string $method The input method to use 'GET' or 'POST'.
* #param string $querystring A custom form input in the query string format.
* #return array $output Returns an array containing the input keys and values.
*/
function bracketless_input( $method, $querystring=null ) {
// Create empty array to
$output = array();
// Get query string from function call
if( $querystring !== null ) {
$query = $querystring;
// Get raw POST data
} elseif ($method == 'POST') {
$query = file_get_contents('php://input');
// Get raw GET data
} elseif ($method == 'GET') {
$query = $_SERVER['QUERY_STRING'];
}
// Separerate each parameter into key value pairs
foreach( explode( '&', $query ) as $params ) {
$parts = explode( '=', $params );
// Remove any existing brackets and clean up key and value
$parts[0] = trim(preg_replace( '(\%5B|\%5D|[\[\]])', '', $parts[0] ) );
$parts[0] = preg_replace( '([^0-9a-zA-Z])', '_', urldecode($parts[0]) );
$parts[1] = urldecode($parts[1]);
// Create new key in $output array if param does not exist.
if( !key_exists( $parts[0], $output ) ) {
$output[$parts[0]] = $parts[1];
// Add param to array if param key already exists in $output
} elseif( is_array( $output[$parts[0]] ) ) {
array_push( $output[$parts[0]], $parts[1] );
// Otherwise turn $output param into array and append current param
} else {
$output[$parts[0]] = array( $output[$parts[0]], $parts[1] );
}
}
return $output;
}
you can try something like this:
foreach($_GET as $slug => $value) {
#whatever you want to do, for example
print $_GET[$slug];
}
I have the following PHP code:
$required_fields = array ('menu_name','visible','position');
foreach($required_fields as $fieldname)
{
if (!isset($_POST[$fieldname]) || empty($_POST[$fieldname]) )
{
$errors [] = $fieldname;
}
}
menu_name, visible and position are variables that are received through the post method.
When the value of visible is zero, it creates an entry into the error array.
What is the best way to detect if a variable is empty when 0 is considered "not empty"?
From PHP's manual:
empty() returns FALSE if var has a
non-empty and non-zero value.
Do something like this:
if ( !IsSet ( $_POST['field'] ) || Trim ( $_POST['field'] ) == '' )
this will ensure that the field is set and that it does not contain a empty string
In essence: it is the empty() that is causing your problems not IsSet()
Since user data is sloppy, I use a custom function that treats empty spaces as non data. It sounds like this will do exactly what you want. This function will consider "0" to be valid (aka non-empty) data.
function isNullOrEmpty( $arg )
{
if ( !is_array( $arg ) )
{
$arg = array( $arg );
}
foreach ( $arg as $key => $value )
{
$value = trim($value);
if( $value == "" || $value == null )
{
return true;
}
}
return false;
}
Please note it supports arrays too but requires that each value in the array contains data, which can be useful as you can just do something like this:
$required = array( $_POST['name'], $_POST['age'], $_POST['weight'] );
if ( isNullOrEmpty($required) )
{
// required data is missing
}
PS: keep in mind this function will fire off PHP warnings if the value isn't set and there's no easy way around that, but you should NOT have warnings enabled in production anyways.
If you want to assure an array key is present you can use array_key_exists() instead of empty()
The check will become a concatenation of is_array() and array_key_exists(), being paranoid of course
Can't you just add another line with something like:
if (!isset($_POST[$fieldname]) || empty($_POST[$fieldname]) )
{
if ($fieldname != 'visible' || $_POST[$fieldname] != 0)
{
$errors [] = $fieldname;
}
}