entre Desarrolladores

Recibe ayuda de expertos

Registrate y pregunta

Es gratis y fácil

Recibe respuestas

Respuestas, votos y comentarios

Vota y selecciona respuestas

Recibe puntos, vota y da la solución

Pregunta

2votos

¿El siguiente código, es seguro contra inyecciones SQL?

No es urgente responder. No es para producción. Tomense su tiempo para responder esta pregunta, porque es larga. Quiero una respuesta que no esté basada en lo que dicen los demas. Y quiero aprender de alguien que sepa de seguridad informática.

Resumiendo, el código es el siguiente:

<?php
    require('./config.php');
    require('./login.php');

    function enviarErrorServidor() {
        http_response_code(500);
        die('Internal Server Error');
    }

    function enviarErrorPeticionHTTP() {
        http_response_code(400);
        die('Bad Request');
    }

    function enviarErrorRecursoNoEncontrado() {
        http_response_code(404);
        die('Not found');
    }

    function verificarNombre($nombre) {
        if (empty($nombre) || !preg_match('/^\w+$/u', (string) $nombre)) {
            enviarErrorPeticionHTTP();
        }
    }

    function verificarClavePrimaria($numero) {
        if (empty($numero) || !ctype_digit((string) $numero)) {
            enviarErrorPeticionHTTP();;
        }
    }

    function enviarDatosComoJSON($datos) {
        header('Content-Type: application/json');
        echo json_encode($datos);
        exit();
    }

    function esCampoRegistro($nombreCampo) {
        return !preg_match('/^_/', (string) $nombreCampo);
    }

    function crearRegistroTabla($nuevoRegistro, $nombreTabla) {
        global $conexion;
        verificarNombre($nombreTabla);
        $nombresCampos = array();
        $valoresCampos = array();
        foreach ($nuevoRegistro as $nombreCampo => $valorCampo) {
            verificarNombre($nombreCampo);
            if (esCampoRegistro($nombreCampo)) {
                if ($nombreCampo == 'id') {
                    enviarErrorPeticionHTTP();
                }
                $nombresCampos[] = $nombreCampo;
                $valoresCampos[] = "'".$conexion->real_escape_string((string) $valorCampo)."'";
            }
        }
        $nombresCampos = '('.implode(', ', $nombresCampos).')';
        $valoresCampos = '('.implode(', ', $valoresCampos).')';
        $operacionSQL = "INSERT INTO $nombreTabla $nombresCampos VALUES $valoresCampos";
        if ($conexion->query($operacionSQL) === TRUE && $conexion->affected_rows === 1) {
            enviarDatosComoJSON($conexion->insert_id);
        } else {
            enviarErrorServidor();
        }
    }

    function modificarRegistroTabla($registroModificado, $nombreTabla) {
        global $conexion;
        verificarNombre($nombreTabla);
        $camposRegistro = array();
        $clavePrimaria = false;
        foreach ($registroModificado as $nombreCampo => $valorCampo) {
            verificarNombre($nombreCampo);
            if ($nombreCampo == 'id') {
                $clavePrimaria = $registroModificado['id'];
            } else {
                if (esCampoRegistro($nombreCampo)) {
                    $camposRegistro[] = "$nombreCampo = '".$conexion->real_escape_string((string) $valorCampo)."'";
                }
            }
        }
        verificarClavePrimaria($clavePrimaria);
        $camposRegistro = implode(', ', $camposRegistro);
        $operacionSQL = "UPDATE $nombreTabla SET $camposRegistro WHERE id = $clavePrimaria";
        if ($conexion->query($operacionSQL) === TRUE) {
            if ($conexion->affected_rows === 1) {
                exit();
            } else {
                enviarErrorRecursoNoEncontrado();;
            }
        } else {
            enviarErrorServidor();
        }
    }

    function eliminarRegistroTabla($clavePrimaria, $nombreTabla) {
        global $conexion;
        verificarNombre($nombreTabla);
        verificarClavePrimaria($clavePrimaria);
        $operacionSQL = "DELETE FROM $nombreTabla WHERE id = $clavePrimaria";
        if ($conexion->query($operacionSQL) === TRUE) {
            if ($conexion->affected_rows === 1) {
                exit();
            } else {
                enviarErrorRecursoNoEncontrado();;
            }
        } else {
            enviarErrorServidor();
        }
    }

    function obtenerRegistroTabla($clavePrimaria, $nombreTabla) {
        global $conexion;
        verificarNombre($nombreTabla);
        verificarClavePrimaria($clavePrimaria);
        $resultado = $conexion->query("SELECT * FROM $nombreTabla WHERE id = $clavePrimaria");
        if ($resultado) {
            if ($resultado->num_rows === 1) {
                enviarDatosComoJSON($resultado->fetch_assoc());
            } else {
                enviarErrorRecursoNoEncontrado();;
            }
        } else {
            enviarErrorServidor();
        }
    }

En el código, la variable $conexion es un objeto de MySQLi declarado anteriormente.
En varias parte del código, hago "type casting" o conversión al tipo "string" o cadena de texto. Eso es para evitar cualquier error que derive en alguna falla de seguridad. Además, MySQL, siempre que encuentre una cadena de texto en los valores de los campos de un registro, la convierte siempre al tipo de dato que corresponde. Lo descubrí hace poco, revisando codigo fuente ajeno.
Usé el método real_escape_string porque hace que el código sea mucho más facil de implementar. Pero tengo una duda existencial: ¿Es realmente segura esta función utilizada de esa manera, con respecto a inyecciones SQL?
Con respecto a las funciones que usan expresiones regulares, utilize el siguiente sitio web para comprobarlas: regex101.com. ¿Hay alguna falla de seguridad en esas expresiones regulares, con respecto a inyecciones SQL?
El archivo login.php, no es importante para el ejemplo, pero si, para la seguridad del sistema. Si no lo incluyo, el usuario no necesitaria iniciar sesion para usar el sistema.
Además, no uso esas funciones solas, sino que tambien realizo otras validaciones y sanitizaciones, previamente para evitar vulnerabilidades XSS.

En el archivo config.php esta la secuencia de conexión a la base de datos MySQL y su código fuente es el siguiente:

<?php
    define('MODO_DESARROLLADOR', true);

    $conexion = new mysqli('127.0.0.1', 'usuario', 'contraseña', 'base-de-datos');

    if ($conexion->connect_errno > 0) {
        if (MODO_DESARROLLADOR) {
            die('Unable to connect to database [' . $conexion->connect_error . ']');
        } else {
            die('Internal Server Error');
        }
    }

    $conexion->set_charset('utf8');

    if (MODO_DESARROLLADOR) {
        header('Access-Control-Allow-Origin: *');
    } else {
        error_reporting(0);
    }

Si el código fuente no es seguro. ¿Cuales son sus fallas de seguridad?

Si debo usar sentencias preparadas (creo que es lo que recomiendan, pero me gustaria saber porqué).¿Es posible implementar el código anterior usando sentencias preparadas? Siendo de esa manera: ¿De que manera podría implementarlo?

1 Respuesta

2votos

magarzon Puntos30650

Voy a intentar contestar a tu pregunta de la mejor manera, aunque en cuestiones de seguridad, nunca se puede dar una respuesta categórica.

Para empezar, el uso de real_escape_string por sí solo no proporciona seguridad, que es algo que muchas personas creen falsamente. Dependiendo de la configuración y/o de cómo construyas tus queries, la función real_escape_string no te va a proteger de nada.

En cuanto a configuración, si la codificación de tu base de datos es utf8, ascii o latin1, en principio no tendrías ningún problema (con otras codificaciones, habría que tener en cuenta otros parámetros de configuración). En cuanto a construcción de las queries, como no usas LIKE (que requeriría posiblemente alguna protección adicional) y como estás encapsulando los valores con ', tampoco habría problemas.

Por lo que respecta a las sentencias preparadas, te pueden ofrecer un nivel adicional de seguridad, pero que realmente no sería necesario en este caso, y además, solo si estableces esta configuración: $conexion->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
En ese caso, se convierte en una verdadera sentencia preparada, en el que la query y los datos son enviados por separado a MySQL, y es MySQL quien construye la sentencia a ejecutar. Si no se establece ese parámetro a false, en realidad lo que hace realmente la función prepare es poco más o menos lo que ya estás haciendo tú, llamar a real_escape_string.

Por último, las expresiones regulares que estás usando no tienen ningún problema de seguridad, ya que al final estás obligando a que solo sean caracteres alfabéticos (lo que no quiere decir que, en general, las expresiones regulares no puedan tener también sus vulnerabilidades, que las tienen)

No has aportado el fichero de login, ten cuidado de tener las mismas precauciones ahí, porque hay gente que protege sus consultas, y luego la más fundamental que es el login no lo protegen.

Edit: Se me olvidaba contestar a cómo convertir tu código a sentencias preparadas. Evidentemente, tienes que seguir construyendo la consulta como lo estás haciendo para lo que es el nombre de tabla y el nombre de los campos, pero luego cambia la parte de los valores de esos campos. En lugar de poner el valor, pon un ?.

Para los select es inmediato:

$stmt = $conexion->prepare("SELECT * FROM $nombreTabla WHERE id = ?");
$resultado = $stmt->execute([$clavePrimaria]);

Para el de inserción sería así:

foreach ($nuevoRegistro as $nombreCampo => $valorCampo) {
            verificarNombre($nombreCampo);
            if (esCampoRegistro($nombreCampo)) {
                if ($nombreCampo == 'id') {
                    enviarErrorPeticionHTTP();
                }
                $nombresCampos[] = $nombreCampo;
                $valoresCampos[] = "'".$conexion->real_escape_string((string) $valorCampo)."'";
            }
        }
        $nombresCampos = '('.implode(', ', $nombresCampos).')';
        $placeholder = '('.implode(', ', array_fill(0, count($valoresCampos), '?')).')';
        $operacionSQL = "INSERT INTO $nombreTabla $nombresCampos VALUES $placeholder";
$conexion->prepare($operacionSQL);
$resultado = $conexion->execute($valoresCampos);

0voto

pedrourday comentado

Muy buena respuesta, me aclaró un monton de dudas. Con respecto a lo de implementar el código fuente con sentencias preparadas o parametrizadas, se me ocurrio una idea de como implementarlo. Lo que me trae problemas, a la hora de implementarlo, es que tengo dudas con el método mysqli_stmt::bind_param. En definitiva, voy a usar sentencias parametrizadas, ya que en algun momento voy a usar LIKE y MATCH AGAINST. Y con respecto al archivo login.php, no es nada de otro mundo, simplemente usa sesiones PHP y se conecta a la misma base de datos MySQL (no tengo dudas de seguridad con respecto a eso).

1voto

magarzon comentado

Perdón, es verdad, que usas las funciones mysqli, no las mysql (la costumbre de que la mayor parte de personas aún usen las segundas).

En ese caso, no haría falta ejecutar $conexion->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);, porque por defecto en mysqli se ejecutan sentencias preparadas "reales".

Y el otro cambio es que en lugar de meter los parámetros como un array en la llamada a execute, se añaden llamando a bind_param.

¿Qué problema tienes con esta función?

0voto

pedrourday comentado

Bien que aclares! Una forma facil de darse cuenta que se usa mysqli y no mysql, es que a diferencia de mysql, mysqli tiene ademas la interface orientada a objetos. En los ejemplos, la variable $conexion, es un objeto de mysqli.
Si uso siempre sentencias parametrizadas o preparadas en el código fuente, ¿No habría ningun problema de seguridad, con respecto a inyecciones SQL? Por si quiero usar el código fuente en producción.
Con respecto a la función bind_param, estoy haciendo pruebas en mi PC (http://127.0.0.1/) y armando una pregunta sobre eso (de ser necesario). Se trata sobre uno de sus parámetros.

Por favor, accede o regístrate para responder a esta pregunta.

Otras Preguntas y Respuestas


...

Bienvenido a entre Desarrolladores, donde puedes realizar preguntas y recibir respuestas de otros miembros de la comunidad.

Conecta