PHP: Uso adecuado de parse_str

La siguiente porción de código es una práctica muy extendida en WordPress, sirve para que las funciones puedan tener un buen número de parámetros opcionales sin que el código sea visualmente feo:

php:
<?php
/* Funciones de WordPress */
function stripslashes_deep($value) {
         $value = is_array($value) ?
                 array_map('stripslashes_deep', $value) :
                 stripslashes($value);

         return $value;
}
function wp_parse_str( $string, &$array ) {
        parse_str( $string, $array );
        if ( get_magic_quotes_gpc() )
                $array = stripslashes_deep( $array );
        return $array;
}
/* Fin */

function get_posts( $args = '' ) {
        $defaults = array(
                'limit' => 5,
                'post_type' => ''
        );
        $args = wp_parse_str($args, $defaults);
       
        extract($args, EXTR_SKIP);
       
        $where = '';
        if ( !empty($post_type) )
                $where = "WHERE post_type = '$post_type'";
       
        $sql = "SELECT * FROM posts $where LIMIT $limit";
        // ejecutar la consulta en una base de datos MySQL
        echo htmlspecialchars($sql);
}

$limit = empty($_GET['limit']) ? 5 : (int) $_GET['limit'];

if (get_magic_quotes_gpc())
        $_GET['type'] = stripslashes($_GET['type']);
// Se usa addslashes sólo para el ejemplo.
// $type = mysql_real_escape_string($_GET['type']);
$type = addslashes($_GET['type']);
get_posts("limit=$limit&post_type=$type");
?>

Sobre el ejemplo mostrado:

  1. ¿Tiene algún problema de seguridad?
  2. ¿Qué pasa si cambiamos la línea resaltada por esta otra, tiene algún problema de seguridad?
    php:
    $where = "WHERE post_type = '". addslashes($post_type) . "'";

Para los interesados en este pequeño quiz, subí una página en la que pueden realizar sus pruebas.

WordPress, XSS y CSRF – Parte 2

En la primera parte vimos un poco de los problemas que tiene usar $_SERVER['PHP_SELF'] sin ningún tipo de validación. Para esta segunda parte, he preparado un pequeño quiz que básicamente refleja los problemas reportados en WordPress.

La siguiente porción de código es una versión resumida del contenido de los archivos wp-includes/vars.php y wp-includes/functions.php (función wp_nonce_ays):

php:
<?php

$PHP_SELF = $_SERVER['PHP_SELF'];
if ( preg_match('#([^/]+\.php)$#', $PHP_SELF, $self_matches) ) {
        $pagina_actual = $self_matches[1];
} else {
        $pagina_actual = 'wp.php';
}

?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="es" lang="es">     
<head>
        <title>Wordpress demo</title>
</head>
<body id="main">
        <form action="<?php echo $pagina_actual; ?>" method="post">
                <input name="foo" id="foo" type="text" tabindex="0" />
                <input name="postback" id="postback" type="submit" value="Enviar" tabindex="1" />
        </form>
</body>
</html>

¿Algún ejemplo que aproveche el bug que existe en el código de prueba? (de preferencia que funcione en varios navegadores)

Nota: Por motivos de seguridad no voy a facilitarles una página de prueba, puesto que por más que ponga los ejemplos en un subdominio, éstos son accesibles también desde el dominio principal, haciendo vulnerable a XSS mi instalación de WordPress 😀 --quise usar Dreamhost, pero para este caso no sirve.

Ejercicio de la Semana: Visualización de perfiles

Continuando con la serie, pongo otro script para que se diviertan.

php:
<?php
include_once './config.php';

if ( empty($_REQUEST['id']) && empty($_REQUEST['nick']) ) {
    die('No user selected');
}

if ( isset($_REQUEST['nick']) ) {
    if ( preg_match('/^[a-z0-9]{3,20}$/i', $_REQUEST['nick']) ) {
        $where = "WHERE nick='" . $bcdb->escape($_REQUEST['nick']) . "'";
    }
    else {
        die('Invalid nick: ' . $_REQUEST['nick']);
    }   
}
elseif ( !preg_match('/\d+/i', $_REQUEST['id']) || ($id = $bcdb->escape($_REQUEST['id'])) < 0 ) {
    die('Invalid user id: ' . $id);
}
else {
    $where = "WHERE id='$id'";
}

if ( ! ($user = $bcdb->get_row("SELECT * FROM users $where")) ) {
    die("User does not exists");
}

header('Content-type: text/html; charset=utf-8');
?>
<html>
<head>
    <title>Profile: <?php echo $user->name; ?></title>
</head>

<body>

<dl>

    <dt>Nombre</dt>
    <dd><?php echo $user->name; ?></dd>

    <dt>Email</dt>
    <dd><?php echo $user->email; ?></dd>
   
    <dt>Página Web</dt>
    <dd><a href="<?php echo $user->url; ?>"><?php echo $user->url; ?></a></dd>
</dl>

</body>

</html>

¿Existe algún problema con el código mostrado? Si es así, ¿cuál o cuáles?

Nota: A sugerencia de Agustí, he puesto una página de prueba. Si desean ver el código fuente de los ficheros php, aumenten una s al nombre. Ej http://test.buayacorp.com/quiz/user.phps

Actualización: Cambios realizados

  • Comillas simples agregadas alrededor de la variable id

Ejercicio de la Semana: Login de usuarios y actualización de datos

A partir de ahora, cada semana haré el intento de poner los quiz los días lunes o martes, así tendremos más tiempo para comentar estos pequeños ejercicios.

Esta vez, se trata de dos páginas: la primera que se encarga de realizar el login de los usuarios y la segunda se encarga de mostrar y actualizar el perfil del usuario que accede a la página.

php:
<?php
session_start();

/*
 session.php:
  Se encarga de validar los datos del usuario y
  que exista una sesión válida para acceder a user.php
*/


include_once './config.php';
include_once './db.php';

if ( !empty($_REQUEST['action']) ) {
        switch ($_REQUEST['action']) {
                case 'login':
                        if ( !empty($_SESSION['user']) ) {
                                header('Location: http://sitio/user.php');
                                exit();
                        }
                       
                        $username = $db->escape($_POST['user']);
                        $pass = md5($secret_key . $_POST['password']);
                       
                        if ( $user = $db->get_row("SELECT * FROM users WHERE user='$username' AND password='$pass'") ) {
                                session_regenerate_id();
                                $_SESSION['user'] = $user;
                                header('Location: http://sitio/user.php');
                                exit();
                        }
                        die('Usuario no válido');
                case 'logout':
                        // ...
        }
}

?>
php:
<?php
/*
 user.php:
   Se encarga de mostrar y actualizar el perfil de un usuario
*/


include_once './session.php';

if ( empty($_SESSION['user']) ) {
        header('Location: http://sitio/session.php');
        exit();
}

if ( !empty($_REQUEST['action']) ) {
        switch ($_REQUEST['action']) {
                case 'update-profile':
                       
                        // Limpiar los datos
                        $_POST['name'] = htmlspecialchars(strip_tags($_POST['name']), ENT_QUOTES, 'utf-8');
                        $_POST['email'] = preg_replace('/[^a-z0-9.@-]/i', '', $_POST['email']);   

                        // Escapar los valores
                        $user = $db->escape($_SESSION['user']->user);
                        $name = $db->escape($_POST['name']);
                        $email = $db->escape($_POST['email']);
                        $password = md5($secret_key . $_POST['password']);
                       
                        $sql = "UPDATE users SET name = '%s', email = '%s', password = '%s' WHERE user = '%s'";
                       
                        $db->query(sprintf($sql, $name, $email, $password, $user));
                       
                        break;
                case 'recover-password':
                        // ...
        }
}

?>

¿Existe algún bug en el código mostrado?

Actualización:

  1. Cambio de $_POST['user'] por $_SESSION['user']->user en user.php
  2. Cambio de ubicación de la comprobación de la variable user en la sesión

¿Todavía queda algún problema de seguridad?

Un bug interesante en WordPress

El presente quiz está basado en un reciente bug de WordPress que fue corregido en la versión 2.0.6 de este CMS

php:
<?php

header('Content-type: text/plain; charset=utf-8;');

if ( empty($_POST['title']) ) {
        die('Parámetros no válidos');
}

// Conexión a la base de datos test
mysql_connect('localhost', 'root', '1234');
mysql_select_db('test');

// Escapar los valores
$title = mysql_real_escape_string($_POST['title']);

// Hacer la conversión si se envía el parámetro charset
if ( !empty($_POST['charset']) ) {
        // Verificar si la extensión mb_string está habilitada
        if ( function_exists('mb_convert_string') ) {
                $title = @mb_convert_encoding($title, 'UTF-8', $_POST['charset']);
        }
        // Verificar si la extensión iconv está habilitada
        elseif ( function_exists('iconv') ) {
                $title = @iconv($_POST['charset'], 'UTF-8', $title);
        }       
}

$sql = "SELECT * FROM wp_posts WHERE post_title = '$title'";

if ( $result = mysql_query($sql) ) {
        while ($row = mysql_fetch_assoc($result)) {
                print_r($row);
        }
       
        mysql_free_result($result);
}

/*
        Muestra el error intencionalmente
*/

if ( $error = mysql_error() ) {
        echo "MySQL: $error\nConsulta: $sql";
}

mysql_close();
?>

¿Dónde se encuentra el error? y ¿cómo explotarlo? (Si desean puedo colgar una página para que hagan las pruebas)