Secretos en php

diciembre 3, 2014

Cualquier web con usuarios registrados debe contar con la funcionalidad de “¿Olvido su contraseña?”.
Implementar dicha funcionalidad es muy sencillo. Veamos: mandaremos al usuario un email con la url de login, seguida de un código.
Por ejemplo: http://www.example.com/login/1.
Esta dirección daría acceso al usuario 1. Problema: es perfectamente visible. Añadamos por tanto, un poco de md5 para enmarañar la url que quedaria asi:
http://www.example.com/login/c4ca4238a0b923820dcc509a6f75849b
Ya no es visible, pero presenta dos problemas: es vulnerable a ataques por diccionario, y ataques de fortuna (alguien puede probar a introducir http://www.example.com/login/ + md5(123).

Solución: combinamos la id del usuario, un campo del propio del usuario (fecha de alta por ejemplo), con una semilla elegida al azar mas la marca de tiempo.
http://www.example.com/login/ + md5( $id . $f_alta . “jhdjdhhj “. date(“Ymd”));

Esta sencilla clave es única para un usuario con determinda id y fecha de alta, para cierta aplicación y para cierto periodo de tiempo (el día de hoy).

Ahora ya podemos incluir el código de nuestra aplicación de ‘palabras secretas’. Os adjunto un sencillo pseudo-codigo:


const SEED = "uiusiui j"; // cualquier valor escogido al azar. Hazlo único para cada instalación.
const SECRET_VALID ="Ymd"; // notación date de PHP.

function semilla_secreta(){
return SEED . date(SECRET_VALID);
}

function secreto( $datos ) {
return md5(implode($datos) . semilla_secreta());
}

// devuelve la id del usuario al que corresponde el secreto.
function check_secreto ($clave) {
$sql = sprintf("SELECT id FROM usuarios WHERE MD5(Concat(id,alta,'%s'))='%s',
addslashes(semilla_secreta()),
addslashes($clave));
return query_var($sql); // debes implementar esta función.
}

// enviar el secreto a cierto email,
function enviar_secreto($email) {
// buscamos los datos (id y alta) de un usuario con el email dado.
$sql = sprintf("SELECT id,alta FROM usuarios WHERE email='%s', addslashes($email))
$datos = query_registro($sql); // debes implementar esta funcion que debe devolver un array.
if ( $datos ) {
...
$mensaje .= sprintf("click aqui", secreto($datos));
mail($email,"Acceso.", $mensaje);
}
}

PD: para los más paranoicos.
Este login todavía es vulnerable a un ataque de fuerza bruta, pero con una posibilidad muy baja de éxito 1 entre 2**128 para cada petición.

Algo de encriptación

abril 6, 2014

PHP tiene una fabulosa librería de encriptación llamada mcrypt. Si no tienes esta librería instalada en tu servidor, o eres de esos paranoicos que desea ver en todo momento como se generan los mensajes codificados, aquí tienes un sencilla librería. Puedes usarla por ejemplo, para encriptar cookies o pequeños mensajes entre usuarios.

La librería tiene 3 funciones útiles:
libreta_de_un_solo_uso($texto) que genera una clave aleatorio con la misma longitud que el texto. Mas información en la wikipedia.
encripta($texto,$llave) y desencripta($texto,$llave)
Encripta y desencripta un texto.La llave puede ser una frase (cuanto mas larga mejor) o puedes generar una libreta de un solo uso con la primera función.
-Hay una cuarta función, cipher, que es en realidad la que hace el trabajo duro.

Leer el resto de esta entrada »

Unas de las preguntas del test de ejemplo de la certificación ZEND es similar a:

1:function f(){
2: $g= 20;
3: global $g;
4: echo $g;
5:}
6:$g=30;
7:f(); //

Para responderla correctamente tienes que entender que el ámbito de global no es la función entera, sino la que empieza en la linea 3. La variable local creada en la linéa 2, es sobre-escrita la línea 3, como una referencia a $GLOBALS[‘g’]. El resultado es “30”;
Parece fácil, pero ¿cuál es el resultado de?


1:function f(){
2: $g= 20;
3: global $g;
4: unset($g);
5:}
6:$g=30;
7:f();
8:echo $g;

¿un error? ¿20 ?
El resultado es 30, ya que la línea 4, no destruye la variable global $GLOBALS[‘g’] sino $g.
Si dentro de una función quieres destruir una variable global debes poner:

function foo(){
..
unset($GLOBALS['NOMBRE_DE_LA_VARIABLE_A_BORRAR']);
..
}

Y que pasa si dentro de una función creamos y destruimos una variable global

function f(){
global $g;
$g=20;
unset($g);
}
f();
echo $g;

¿el resultado es un error o 20? Global es una construcción del lenguaje que equivaldría mas o menos a:

if ( !isset($GLOBALS['g']) {
$GLOBALS['g']= NULL;
}
$g= &$GLOBALS['g'];

Así que el resultado es, 20.

Si quieres conocer certificarte en PHP, deberías saber:
– empty evalúa una variable y nos dice si esta vacía: es 0, “0”, “” (cadena vacía), null, o un array vacío. (boolean) evalúa expresiones del mismo modo que empty, aunque al revés: devuelve true para cualquier valor que no sea 0,”0″,””, NULL, o un array vacío.
– is_null devuelve true si la expresión es NULL, igual que isset que evalua si la variable es null, o no ha sido inicializada.

Lo interesante:
– empty no es una función. Es un construcción del lenguaje; admite un único parámetro que debe ser una variable, la clave de un array o una propiedad.
– isset tampoco es una función, pero la gente olvida que puede aceptar mas de un parámetro, aunque todos deben ser variables (como empty).
– empty e isset no generan error al evaluar una variable inexistente.
– is_null y (boolean) si generan un error al evaluar una variable inexistente.

Resumiendo:

empty(20); // GENERA ERROR
empty(UNA_CONSTANTE); // GENERA ERROR

isset(E_WARNING) ; // GENERA_ERROR
isset($a,$b[1],$c->p) ; // es valido!!

is_null($a) // GENERA un warning si $a no existe.
(boolean) $a+10; // GENERA un warning si $a no existe.

Aquí tenéis dos funciones para la manipulación y análisis de sentencias SQL SELECT.
La función “sql_select2clauses” extrae en un array las clausulas SQL de un sentencia. Podéis cambiar una de ellas y volver a obtener la sentencia con la función inversa “sql_clauses2select”.

Por ejemplo:
– podéis cambiar el orden de la sentencia.
– añadir un limite.
– obtener la sql para contar los registros.

Códigos de ejemplos:
// para contar los registros de una SQL
$clausulas = sql_select2clauses("SELECT id,name FROM users WHERE tipo=1");
$clausuas["SELECT"] = "count(id)";
$sql = sql_clauses2select($clausulas);

// cambiar la pagina
$clausulas = sql_select2clauses("SELECT id,name FROM users WHERE tipo=1 LIMIT 2,25");
$clausuas["LIMIT"] = "3,25";
$sql = sql_clauses2select($clausulas);

// cambiar el orden
$clausulas = sql_select2clauses("SELECT id,name FROM users WHERE tipo=1 LIMIT 2,25");
$clausuas["ORDER BY"] = "name DESC";
$sql = sql_clauses2select($clausulas);

Ahora, las librerias


function sql_clauses2select ($clauses) {
	$orderedClauses = array("SELECT","FROM","WHERE", "GROUP","HAVING","ORDER BY","LIMIT");
	$parts=array("SELECT"=>"SELECT *");
	foreach ($orderedClauses as $clause){
		if ( isset($clauses[$clause]) ){
			$parts[$clause] = $clause . " " . $clauses[$clause];
		}
	}
	return implode(" ",$parts);
}


function sql_select2clauses($sql){	
	$state = 0;	//0=normal 1=over quote 2=over double quote
	$token ="";	
	$clauses = array("FROM","WHERE", "GROUP","HAVING","ORDER BY","LIMIT");
	$founds=false;	
	$previous="";
	
	$sql= trim($sql);
	if ( stripos($sql,"SELECT")!==0 ){
		return false;
	}
		
	for($i= strlen($sql)-1;$i>=0;$i--){
		$char= $sql[$i];
		if ( $state==0 ){
			if ( $char==" " || $char=="," ) { 			
				$token = strtoupper($token);								
				if ($token == "BY" && strtoupper(substr($sql,$i-5,5))=="ORDER") {
					$i -= 5; //5 = length order
					$founds["ORDER BY"]= trim(substr($previous,2)); // 2 length of "BY;
					$previous="";				
				} elseif ( in_array($token,$clauses)) {
					$founds[$token]= trim(substr($previous,strlen($token))); //extract $token;
					$previous="";				
				}				
				$token ="";
			} else {
				$token = $char .$token;
			}
			
		} elseif ( ($state==1 && $char=="'") OR ($state==2 && $char=='"') ){
			$state=0;
		}
		$previous = $char . $previous;
	}
	$founds["SELECT"] = trim(substr($previous,7)); 	
	
   return array_reverse($founds);
}

Si necesitas una función para clonar registros, aquí tienes unas. La idea es sencilla: usar SQL para clonar un registro. En esencia es algo así:


INSERT INTO tabla (SELECT * FROM tabla WHERE id=..)

El problema es que la SQL anterior no funciona si tienes un campo clave (99% de las tablas, o un campo único). Así que toca refinar mas la función.


function mysql_clonar_registro ( $tabla, $clave ) {

   // limpieza parámetros
   $tabla= mysql_real_escape_string($tabla);
   $clave= mysql_real_escape_string($clave);

   // obtener lista de campos, no únicos
   $rsCampos = mysql_query("SHOW COLUMNS FROM $tabla");
   $campos= array();
   $campoClave ="";
   while ( $campo = mysql_fetch_array($rsCampos) ){

       if ( $campo["Key"] == "PRI" ){
           $campoClave = $campo[0];
       }
       $campos[] =  $campo["Key"] == "PRI" || $campo["Key"] == "UNI" ? "NULL":    $campo[0];
   }
   mysql_free_result ( $rsCampos );

   // clonar el registro mediante una SQL
   if ( $campoClave && count($campos)>0 ) {
       $SQL = sprintf( "INSERT INTO $tabla ( SELECT %s FROM $tabla WHERE %s='%s' )",
           implode(",",$campos),
           $campoClave,
           $clave );
       mysql_query ($SQL);
       return mysql_affected_rows();
   }
   return false;
}

Pretty-urls en PHP

marzo 23, 2011

Usar las llamadas pretty-url o url elegantes es útil para dos cosas:
– hace más usables las direcciones de nuestras páginas web. Eso es importante cuando la gente guarda la página es sus marcadores, o en del.icio.us, o al reenvíar a un amigo.
– las leyendas urbanas dicen que así se mejora el ranking para buscadores. En realidad, los buscadores escarban igual en una web con pretty-url que si ella (bueno, parece que ahora en 2014 google puntua mejor a las web con url amigables).
Lo que si resulta más fácil es revisar los informes de google: ¿que te dice que la página mas vista es http://www.example.org?id=100.

Para implementar estas URL solo hay que:
– definir el fichero .htaccess adecuadamente
– añadir un poco de programación.
Leer el resto de esta entrada »

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.