¿Qué tal está mi formulario?

Hola a tod@s,

Estoy empezando con esto del php. Hice un formulario hace tiempo y puse todo en javascript (checkbox, validaciones, etc).

Este es el código del action:

<?php 
$nombre = $_POST['nombre']; 
$apellidos = $_POST['apellidos']; 
$dni = $_POST['dni']; 
$mail = $_POST['mail']; 
$telefono = $_POST['telefono']; 
$direccion = $_POST['direccion']; 
$ip = $_SERVER['REMOTE_ADDR']; 

$header = 'From: ' . $mail . " \r\n"; 
$header .= "X-Mailer: PHP/" . phpversion() . " \r\n"; 
$header .= "Mime-Version: 1.0 \r\n"; 
$header .= "Content-Type: text/plain"; 

$mensaje = "Este mensaje fue enviado por " . $nombre . ", con apellidos " . $apellidos . " \r\n"; 
$mensaje .= "Condiciones: " . $_POST['condiciones'] . " \r\n"; 
$mensaje .= "Su e-mail es: " . $mail . " \r\n"; 
$mensaje .= "Dni: " . $_POST['dni'] . " \r\n"; 
$mensaje .= "Teléfono: " . $_POST['telefono'] . " \r\n"; 
$mensaje .= "Dirección: " . $_POST['direccion'] . " \r\n"; 
$mensaje .= "Su ip: " . $ip . " \r\n"; 
$mensaje .= "Enviado el " . date('d/m/Y', time()); 

$para = '[correo eliminado]'; 
$asunto = 'Cliente nuevo'; 

mail($para, $asunto, utf8_decode($mensaje), $header); 

echo 'Mensaje enviado'; 

?>


Un amigo me comentó que no era seguro, que había que filtrar variables para evitar inyecciones y ataques.
La verdad que no entiendo mucho de como limpiar, filtrar y demás. He visto muchos manuales de como hacerlo pero no porqué realmente y que se evita.
Si me pueden ayudar un poco a arrancar sería genial.

Saludos!
Hola:

La inyección se refiere a que un hacker podría, por ejemplo, en el campo donde debe poner su nombre, escribir echo "Mi nombre es tanto"; y PHP lo ejecutaría sin más, pues las variables no han sido filtradas. Esto no hará ningún daño, pero si escribiera, por ejemplo, DROP TABLE usuarios; y esta tabla realmente existiera, entonces se borraría toda la información (si estuvieras recién comenzando el sitio daría igual, pero ¿te imaginas perder la información recopilada durante años por no filtrar las variables?).

Esta bien que hayas validado con Javascript pero, ¿que pasa si el usuario tiene deshabilitado Javascript? Los datos serán enviados al servidor sin validar.

Entonces, para prevenir este tipo de ataques, siempre debes validar que lo ingresado sea lo que necesitas por el lado del servidor, o sea, si necesitas una cadena, entonces asegurarte que lo ingresado sea una cadena; si necesitas un número (de teléfono, por ejemplo), asegurarte de que este sea un número, etc., y para ello existen varias formas:

Las funciones is_xxx( $variable ) que puedes ver aquí.
Expresiones regulares
La función filter_var();

Esta última es la que más utilizo. Para validar los campos para enviar un email, lo mínimo que necesitas es el mensaje y el email del remitente, pues sin este a quién le respondes, ¿no? Entonces, limpiando las variables quedaría así:

// Yo utilizo $flags para los strings. Estas "banderas" le dicen a filter_var que codifique el caracter ampersand (&) y los caracteres (basándose en [URL="http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters"]ASCII[/URL])
// mayores a 127 y menores a 32:
$flags = FILTER_FLAG_ENCODE_AMP & FILTER_FLAG_ENCODE_LOW & FILTER_FLAG_ENCODE_HIGH;

// $out lo utilizo para mostrar un mensaje con los errores
$out = '';

// SIEMPRE valida el ingreso
// Con lo siguiente harás que todos los campos sean obligatorios.
// Si quieres que alguno NO sea obligatorio, entonces quítalo del if:
if( isset( $_POST['nombre'] ) &&
    isset( $_POST['apellidos'] ) &&
    isset( $_POST['dni'] ) &&
    isset( $_POST['mail'] ) &&
    isset( $_POST['telefono'] ) &&
    isset( $_POST['direccion'] ) ) {
  // Ya que no existe un estandar para los nombres, no se puede validar, pero sí puedes obligar al usuario a poner algo, isset( $variable ) se encarga
  // de que exista contenido en la variable:
  $nombre = filter_var( $_POST['nombre'], FILTER_SANITIZE_STRING, $flags );

  // Lo mismo para los apellidos:
  $apellidos = filter_var( $_POST['apellidos'], FILTER_SANITIZE_STRING, $flags );

  // El DNI tiene un formato especial, el cual deberías validar. Busca en google como hacerlo (si es que no sabes), pues yo sólo se validar el RUT chileno.
  $dni = $_POST['dni'];
  
  if( ! funcion_validar_dni( $dni ) )
    $out .= '<p>El DNI ingresado no es válido';

  $mail = $_POST['mail'];

  if( filter_var( $mail, FILTER_VALIDATE_EMAIL ) ) // Si es válido
    $mail = filter_var( $mail, FILTER_SANITIZE_EMAIL ); // Se limpia
  else
    $out .= '<p>El email no es válido.</p>'; // De lo contrario se añade un error.

  // El teléfono también se puede validar, solo que a través de Expresiones Regulares.

  $min_len = 6;
  $max_len = 9;
  // Esta expresión regular validaría un número telefónico cuyo
  // largo mínimo sería $min_len y el máximo $max_len. Debe comenzar con uno o dos números y puede incluir o no un guión
  // Ej.: 02-1234567, sería un número válido
  $regex = '/^(\d){1,2}-?(\d){' . $min_len . ',' . $max_len . '}$/';
  $telefono = $_POST['telefono'];

  if( ! preg_match( $regex, $telefono ) )
    $out .= '<p>Ingrese un número de teléfono con el siguiente formato: 00-1234567';

  // La dirección también se podría validar con expresiones regulares, pero es algo arriesgado
  // pues cualquier excepción a la regla impediría que el usuario pueda ingresar su dirección:
  $direccion = filter_var( $_POST['direccion'], FILTER_SANITIZE_STRING, $flags );

  if( empty( $out ) ) { // Si $out esta vacío, entonces se envia el email, pues significa que no hay errores (en teoría).
    $ip = $_SERVER['REMOTE_ADDR'];  

    $header = 'From: ' . $mail . " \r\n";
    $header .= "X-Mailer: PHP/" . phpversion() . " \r\n";
    $header .= "Mime-Version: 1.0 \r\n";
    $header .= "Content-Type: text/plain";

    $mensaje = "Este mensaje fue enviado por " . $nombre . ", con apellidos " . $apellidos . " \r\n";
    $mensaje .= "Condiciones: " . $_POST['condiciones'] . " \r\n";
    $mensaje .= "Su e-mail es: " . $mail . " \r\n";
    $mensaje .= "Dni: " . $_POST['dni'] . " \r\n";
    $mensaje .= "Teléfono: " . $_POST['telefono'] . " \r\n";
    $mensaje .= "Dirección: " . $_POST['direccion'] . " \r\n";
    $mensaje .= "Su ip: " . $ip . " \r\n";  
    $mensaje .= "Enviado el " . date('d/m/Y', time());  

    $para = '[correo eliminado]';
    $asunto = 'Cliente nuevo';

    if( mail($para, $asunto, utf8_decode($mensaje), $header) )
      $out = 'Mensaje Enviado';
    else
      $out = 'Ocurrió un error al enviar el mensaje';
  }
} else {
  $out = '<p>Por favor, llene todos los campos marcados con un asterisco.</p>';
}

echo $out;

De esta forma tendrás un formulario para enviar email con menos probabilidades de ser "atacado". Obviamente hay que tener en cuenta que, dependiendo de la fama de tu sitio web, este puede o no ser atacado, por lo que si estas recién comenzando es MUY poco probable que seas objetivo de un hacker.

Espero te sea de ayuda, sino... escribí en vano XD.

Saludos :D.
Impresionante, te agradezco mucho la explicación.

El formulario solo envía unos datos al email, nada de base de datos ¿Realmente necesito seguridad por inyección de código?
Es increíble la de formas que hay de programar un "simple" formulario...sigo guías y veo que hay variables más complejas.

Estaba viendo este video: http://www.youtube.com/watch?v=DAt8LhTqYMk
Me gusta mucho como los errores se muestran en la misma página...voy a ver si puedo hacerlo a partir de lo que me has puesto.

Mil gracias!
Hola de nuevo,

Tengo un problema con las casillas de nombre, apellido y dirección. Parece que no se obliga a escribir algo, si se queda en blanco sigue enviando el mensaje.

El código queda así:

				<?php
// $flags para los strings. Estas "banderas" le dicen a filter_var que codifique el caracter ampersand (&) y los caracteres (basándose en ASCII)
// mayores a 127 y menores a 32:
$flags = FILTER_FLAG_ENCODE_AMP & FILTER_FLAG_ENCODE_LOW & FILTER_FLAG_ENCODE_HIGH;

// $out para mostrar un mensaje con los errores
$out = '';

// SIEMPRE valida el ingreso
// Con lo siguiente harás que todos los campos sean obligatorios.
// Si quieres que alguno NO sea obligatorio, entonces quítalo del if:
if( isset( $_POST['nombre'] ) &&
    isset( $_POST['apellidos'] ) &&
    isset( $_POST['mail'] ) &&
    isset( $_POST['condiciones'] ) &&
    isset( $_POST['telefono'] ) &&
    isset( $_POST['direccion'] ) ) {
    
    
  // Ya que no existe un estandar para los nombres, no se puede validar, pero sí puedes obligar al usuario a poner algo, isset( $variable ) se encarga
  // de que exista contenido en la variable:
  $nombre = filter_var( $_POST['nombre'], FILTER_SANITIZE_STRING, $flags );

  // Lo mismo para los apellidos:
  $apellidos = filter_var( $_POST['apellidos'], FILTER_SANITIZE_STRING, $flags );
 
  // email
  $mail = $_POST['mail'];

  if( filter_var( $mail, FILTER_VALIDATE_EMAIL ) ) // Si es válido
    $mail = filter_var( $mail, FILTER_SANITIZE_EMAIL ); // Se limpia
  else
    $out .= '<p>El email no es válido.</p>'; // De lo contrario se añade un error.

  // El teléfono también se puede validar, solo que a través de Expresiones Regulares.

  $min_len = 6;
  $max_len = 9;
  // Esta expresión regular validaría un número telefónico cuyo
  // largo mínimo sería $min_len y el máximo $max_len. Debe comenzar con uno o dos números y puede incluir o no un guión
  // Ej.: 02-1234567, sería un número válido
  $regex = '/^(\d){1,2}-?(\d){' . $min_len . ',' . $max_len . '}$/';
  $telefono = $_POST['telefono'];

  if( ! preg_match( $regex, $telefono ) )
    $out .= '<p>Ingrese un número de teléfono con el siguiente formato: 00-1234567';

  // La dirección también se podría validar con expresiones regulares, pero es algo arriesgado
  // pues cualquier excepción a la regla impediría que el usuario pueda ingresar su dirección:
  $direccion = filter_var( $_POST['direccion'], FILTER_SANITIZE_STRING, $flags );

  if( empty( $out ) ) { // Si $out esta vacío, entonces se envia el email, pues significa que no hay errores (en teoría).
    $ip = $_SERVER['REMOTE_ADDR'];  

    $header = 'From: ' . $mail . " \r\n";
    $header .= "X-Mailer: PHP/" . phpversion() . " \r\n";
    $header .= "Mime-Version: 1.0 \r\n";
    $header .= "Content-Type: text/plain";

    $mensaje = "Este mensaje fue enviado por " . $nombre . ", con apellidos " . $apellidos . " \r\n";
    $mensaje .= "Condiciones: " . $_POST['checkbox'] . " \r\n";
    $mensaje .= "Su e-mail es: " . $mail . " \r\n";
    $mensaje .= "Dni: " . $_POST['dni'] . " \r\n";
    $mensaje .= "Teléfono: " . $_POST['telefono'] . " \r\n";
    $mensaje .= "Dirección: " . $_POST['direccion'] . " \r\n";
    $mensaje .= "Su ip: " . $ip . " \r\n";  
    $mensaje .= "Enviado el " . date('d/m/Y', time());  

    $para = '[correo eliminado]';
    $asunto = 'Cliente nuevo';

    if( mail($para, $asunto, utf8_decode($mensaje), $header) )
      $out = 'Mensaje Enviado';
    else
      $out = 'Ocurrió un error al enviar el mensaje';
  }
} else {
  $out = '<p>Por favor, llene todos los campos marcados con un asterisco.</p>';
}

echo $out;

?>
			
			<table border="0" width="925" cellspacing="0" cellpadding="0" height="336">
				<tr>
					<td align="center" valign="top">
					&nbsp;<p><b>&nbsp;FORMULARIO</b></p>
					<p>Todos los campos son obligatorios</p>
					<table border="0" width="49%" cellspacing="0" cellpadding="0">
						<tr>					
						
						<form name="fvalida" method="post" action="formulario.php">
							<td align="right">&nbsp;    <label for="nombre">Nombre</label>
</td>
							<td>&nbsp;<input type="text" name="nombre" id="nombre" value="<? echo $nombre; ?>"> </td>
						</tr>
						<tr>
							<td align="right">&nbsp;<label for="apellidos" >Apellidos</label></td>
							<td>&nbsp;<input type="text" name="apellidos" id="apellidos" value="<? echo $apellidos; ?>"></td>
						</tr>
						
						<tr>
							<td align="right">&nbsp;    <label for="mail" >Email</label></td>
							<td>&nbsp;<input type="text" name="mail" id="mail" value="<? echo $mail; ?>"></td>
						</tr>
						<tr>
							<td align="right">&nbsp;    <label for="telefono" >Teléfono</label>
</td>
							<td>&nbsp;<input type="text" name="telefono" id="telefono" value="<? echo $telefono; ?>"></td>
						</tr>
						<tr>
							<td align="right">&nbsp;    <label for="direccion" >Dirección</label>
</td>
							<td>&nbsp;<input type="text" name="direccion" id="direccion" value="<? echo $direccion; ?>"></td>
						</tr>
					</table>

  <p><label for="condiciones">Acepta las
  
	<a href="../01/pagina_condiciones.html">condiciones 
	de contrato</a></label><input type="checkbox" name="condiciones" />
    
    <br>
    

    <input type="submit" name="enviar" id="enviar" value="Enviar" /><br></form>
    
    
  </p>
</td>
				</tr>
			</table>
			


He añadido un checkbox, el cual va muy bien.
También he creado en las casillas un value con la variable, ejemplo:

value="<? echo $nombre; ?>"
Pero se borra al no marcar el checkbox.


¿alguna idea? :):)
El formulario solo envía unos datos al email, nada de base de datos ¿Realmente necesito seguridad por inyección de código?

Siempre es necesario, pues también pueden inyectar código para extraer otra información, no sólo desde las bases de datos.

<?php
// $flags para los strings. Estas "banderas" le dicen a filter_var que codifique el caracter ampersand (&) y los caracteres (basándose en ASCII)
// mayores a 127 y menores a 32:
$flags = FILTER_FLAG_ENCODE_AMP & FILTER_FLAG_ENCODE_LOW & FILTER_FLAG_ENCODE_HIGH;

// $out para mostrar un mensaje con los errores
$out = '';

$nombre = isset( $_POST['nombre'] ) ? $_POST['nombre']: '';
$appellido = isset( $_POST['apellidos'] ) ? $_POST['apellidos']: '';
$mail = isset( $_POST['mail'] ) ? $_POST['mail']: '';
$telefono = isset( $_POST['telefono'] ) ? $_POST['telefono']: '';
$direccion = isset( $_POST['direccion'] ) ? $_POST['direccion']: '';

// SIEMPRE valida el ingreso
// Con lo siguiente harás que todos los campos sean obligatorios.
// Si quieres que alguno NO sea obligatorio, entonces quítalo del if:
if( ! empty( $nombre ) &&
    ! empty( $apellido ) &&
    ! empty( $mail ) &&
    isset( $_POST['condiciones'] ) &&
    ! empty( $telefono ) &&
    ! empty( $direccion ) ) {
    
    
  // Ya que no existe un estandar para los nombres, no se puede validar, pero sí puedes obligar al usuario a poner algo, isset( $variable ) se encarga
  // de que exista contenido en la variable:
  $nombre = filter_var( $_POST['nombre'], FILTER_SANITIZE_STRING, $flags );

  // Lo mismo para los apellidos:
  $apellidos = filter_var( $_POST['apellidos'], FILTER_SANITIZE_STRING, $flags );
 
  // email
  $mail = $_POST['mail'];

  if( filter_var( $mail, FILTER_VALIDATE_EMAIL ) ) // Si es válido
    $mail = filter_var( $mail, FILTER_SANITIZE_EMAIL ); // Se limpia
  else
    $out .= '<p>El email no es válido.</p>'; // De lo contrario se añade un error.

  // El teléfono también se puede validar, solo que a través de Expresiones Regulares.

  $min_len = 6;
  $max_len = 9;
  // Esta expresión regular validaría un número telefónico cuyo
  // largo mínimo sería $min_len y el máximo $max_len. Debe comenzar con uno o dos números y puede incluir o no un guión
  // Ej.: 02-1234567, sería un número válido
  $regex = '/^(\d){1,2}-?(\d){' . $min_len . ',' . $max_len . '}$/';
  $telefono = $_POST['telefono'];

  if( ! preg_match( $regex, $telefono ) )
    $out .= '<p>Ingrese un número de teléfono con el siguiente formato: 00-1234567';

  // La dirección también se podría validar con expresiones regulares, pero es algo arriesgado
  // pues cualquier excepción a la regla impediría que el usuario pueda ingresar su dirección:
  $direccion = filter_var( $_POST['direccion'], FILTER_SANITIZE_STRING, $flags );

  // Aquí validas si se aceptaron las condiciones o no (si se aceptaron, entonces el valor será igual a on):
  if( $_POST['condiciones'] !== 'on' )
    $out .= '<p>Debe aceptar las condiciones</p>';

  if( empty( $out ) ) { // Si $out esta vacío, entonces se envia el email, pues significa que no hay errores (en teoría).
    $ip = $_SERVER['REMOTE_ADDR'];  

    $header = 'From: ' . $mail . " \r\n";
    $header .= "X-Mailer: PHP/" . phpversion() . " \r\n";
    $header .= "Mime-Version: 1.0 \r\n";
    $header .= "Content-Type: text/plain";

    $mensaje = "Este mensaje fue enviado por " . $nombre . ", con apellidos " . $apellidos . " \r\n";
    $mensaje .= "Condiciones: " . $_POST['checkbox'] . " \r\n";
    $mensaje .= "Su e-mail es: " . $mail . " \r\n";
    $mensaje .= "Dni: " . $_POST['dni'] . " \r\n";
    $mensaje .= "Teléfono: " . $_POST['telefono'] . " \r\n";
    $mensaje .= "Dirección: " . $_POST['direccion'] . " \r\n";
    $mensaje .= "Su ip: " . $ip . " \r\n";  
    $mensaje .= "Enviado el " . date('d/m/Y', time());  

    $para = '[correo eliminado]';
    $asunto = 'Cliente nuevo';

    if( mail($para, $asunto, utf8_decode($mensaje), $header) )
      $out = 'Mensaje Enviado';
    else
      $out = 'Ocurrió un error al enviar el mensaje';
  }
} else {
  $out = '<p>Por favor, llene todos los campos marcados con un asterisco.</p>';
}

echo $out;

?>

Ahora sí debería funcionar todo. El problema era que estaba validando sólo que existieran las variables (isset( $_POST['algo'] )) y no si estaban vacías o no (empty( $variable )) :P.

Pero se borra al no marcar el checkbox.

No sé por qué no se borran al marcar la casilla, pero el problema es que las variables no existen donde las estas mostrando (echo $variable): se crean después de entrar al if( ! empty( $variable ) ) y se crean dentro de este, por lo que, en teoría, no estarían disponibles "mas abajo". Con las modificaciones que le hice debería funcionar bien :D.
Aunque no uses base de datos y envíe un email, podría hacer una inyección de código mediante la que te robaría las cookies y accedería a un sitio determinado podiéndose hacer pasar por ti, acceder a tu información...
Yo suelo no complicarme y usar strip_tags($variable_a_filtrar) ya que elimina todo rastro de código que pudiera ser malicioso, un ejemplo:
<?php

// Cadena a filtrar
$cadena = "<h1>Hola, si no me filtras me veré como un encabezado, sino, apareceré pequeñito :(</h1>";
// Almacenamos la variable $cadena ya sin todo el html, que vendría a ser el encabezado h1
$cadena_filtrada = strip_tags($cadena);

// Esto devolvería en grande "Hola, si no me filtras me veré como un encabezado, sino, apareceré pequeñito :(", es decir, no filtraría el HTML y sería vulnerable
echo $cadena;

// Esto eliminaría todo el HTML de $cadena y se vería bien y no podrían inyectarte código malicioso
echo $cadena_filtrada;

?>