¿Cómo pueden las declaraciones preparadas proteger de los ataques de inyección SQL?

172

¿Cómo nos ayudan las declaraciones preparadas a prevenir ataques de inyección SQL ?

Wikipedia dice:

Las declaraciones preparadas son resistentes frente a la inyección de SQL, porque los valores de los parámetros, que se transmiten más tarde utilizando un protocolo diferente, no necesitan ser escapados correctamente. Si la plantilla de declaración original no se deriva de una entrada externa, la inyección SQL no puede ocurrir.

No puedo ver la razón muy bien. ¿Cuál sería una explicación simple en inglés fácil y algunos ejemplos?

Aan
fuente

Respuestas:

290

La idea es muy simple: la consulta y los datos se envían al servidor de la base de datos por separado .
Eso es todo.

La raíz del problema de inyección SQL está en la mezcla del código y los datos.

De hecho, nuestra consulta SQL es un programa legítimo . Y estamos creando dicho programa dinámicamente, agregando algunos datos sobre la marcha. Por lo tanto, los datos pueden interferir con el código del programa e incluso alterarlo, como lo muestra cada ejemplo de inyección SQL (todos los ejemplos en PHP / Mysql):

$expected_data = 1;
$query = "SELECT * FROM users where id=$expected_data";

producirá una consulta regular

SELECT * FROM users where id=1

mientras este código

$spoiled_data = "1; DROP TABLE users;"
$query        = "SELECT * FROM users where id=$spoiled_data";

producirá una secuencia maliciosa

SELECT * FROM users where id=1; DROP TABLE users;

Funciona porque estamos agregando los datos directamente al cuerpo del programa y se convierte en una parte del programa, por lo que los datos pueden alterar el programa y, dependiendo de los datos pasados, tendremos una salida regular o una tabla userseliminada.

Si bien en el caso de declaraciones preparadas no modificamos nuestro programa, permanece intacto.
Ese es el punto.

Estamos enviando un programa al servidor primero

$db->prepare("SELECT * FROM users where id=?");

donde los datos se sustituyen por alguna variable llamada parámetro o marcador de posición.

Tenga en cuenta que exactamente la misma consulta se envía al servidor, ¡sin ningún dato! Y luego enviamos los datos con la segunda solicitud, esencialmente separados de la consulta en sí:

$db->execute($data);

por lo que no puede alterar nuestro programa y hacer daño.
Bastante simple, ¿no es así?

Lo único que tengo que agregar que siempre se omite en todos los manuales:

Las declaraciones preparadas pueden proteger solo literales de datos , pero no se pueden usar con ninguna otra parte de consulta.
Entonces, una vez que tengamos que agregar, por ejemplo, un identificador dinámico , un nombre de campo, por ejemplo, las declaraciones preparadas no pueden ayudarnos. He explicado el asunto recientemente , así que no me repetiré.

Su sentido común
fuente
2
"por ejemplo, PDO por defecto no usa declaraciones preparadas" - no es exactamente cierto, porque PDO emula declaraciones preparadas solo para controladores que no admiten dicha función.
pinepain
3
@ zaq178miami: "PDO emula declaraciones preparadas solo para controladores que no admiten la función" - no es exactamente cierto. MySQL ha soportado declaraciones preparadas desde hace bastante tiempo. El controlador PDO también. Pero aún así, las consultas MySQL todavía fueron preparadas por PDO por defecto, la última vez que lo verifiqué.
cHao
9
¿Qué es diferente entre $spoiled_data = "1; DROP TABLE users;"-> $query = "SELECT * FROM users where id=$spoiled_data";, en comparación con: $db->prepare("SELECT * FROM users where id=?");-> $data = "1; DROP TABLE users;"-> $db->execute($data);. ¿No harán lo mismo?
Juha Untinen
14
@Juha Untinen Los datos pueden ser cualquier cosa. No analizará los datos. Eso es DATOS, no el comando. Entonces, incluso si $ data contiene comandos sql, no se ejecutará. Además, si la identificación es un número, el contenido de la cadena generará un informe o valor cero.
Soley
21

Aquí está SQL para configurar un ejemplo:

CREATE TABLE employee(name varchar, paymentType varchar, amount bigint);

INSERT INTO employee VALUES('Aaron', 'salary', 100);
INSERT INTO employee VALUES('Aaron', 'bonus', 50);
INSERT INTO employee VALUES('Bob', 'salary', 50);
INSERT INTO employee VALUES('Bob', 'bonus', 0);

La clase Inject es vulnerable a la inyección SQL. La consulta se pega dinámicamente junto con la entrada del usuario. La intención de la consulta era mostrar información sobre Bob. Ya sea salario o bonificación, según la aportación del usuario. Pero el usuario malintencionado manipula la entrada que corrompe la consulta agregando el equivalente de un 'o verdadero' a la cláusula where para que se devuelva todo, incluida la información sobre Aaron que se suponía que estaba oculta.

import java.sql.*;

public class Inject {

    public static void main(String[] args) throws SQLException {

        String url = "jdbc:postgresql://localhost/postgres?user=user&password=pwd";
        Connection conn = DriverManager.getConnection(url);

        Statement stmt = conn.createStatement();
        String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='" + args[0] + "'";
        System.out.println(sql);
        ResultSet rs = stmt.executeQuery(sql);

        while (rs.next()) {
            System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
        }
    }
}

Al ejecutar esto, el primer caso es con uso normal, y el segundo con la inyección maliciosa:

c:\temp>java Inject salary
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary'
salary 50

c:\temp>java Inject "salary' OR 'a'!='b"
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary' OR 'a'!='b'
salary 100
bonus 50
salary 50
bonus 0

No debe compilar sus sentencias SQL con concatenación de cadenas de entrada del usuario. No solo es vulnerable a la inyección, sino que también tiene implicaciones de almacenamiento en caché en el servidor (la instrucción cambia, por lo que es menos probable que obtenga un acierto en la caché de la instrucción SQL, mientras que el ejemplo de enlace siempre ejecuta la misma instrucción).

Aquí hay un ejemplo de enlace para evitar este tipo de inyección:

import java.sql.*;

public class Bind {

    public static void main(String[] args) throws SQLException {

        String url = "jdbc:postgresql://localhost/postgres?user=postgres&password=postgres";
        Connection conn = DriverManager.getConnection(url);

        String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?";
        System.out.println(sql);

        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.setString(1, args[0]);

        ResultSet rs = stmt.executeQuery();

        while (rs.next()) {
            System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
        }
    }
}

Ejecutar esto con la misma entrada que el ejemplo anterior muestra que el código malicioso no funciona porque no hay PaymentType que coincida con esa cadena:

c:\temp>java Bind salary
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?
salary 50

c:\temp>java Bind "salary' OR 'a'!='b"
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?
Glenn
fuente
¿Usar una declaración preparada del programa que se conecta a la base de datos tiene el mismo efecto que usar una declaración preparada que es parte de la base de datos? Por ejemplo, Postgres tiene su propia declaración preparada y ¿usarla evitaría la inyección de SQL? postgresql.org/docs/9.2/static/sql-prepare.html
Celeritas
@Celeritas No tengo una respuesta definitiva para Postgresql. Mirando los documentos, parece que el efecto es el mismo. PREPAREcrea una declaración con nombre fijo que ya está analizada (es decir, la declaración ya no va a cambiar independientemente de la entrada) mientras EXECUTEejecuta la declaración con nombre que vincula los parámetros. Dado que PREPAREsolo tiene una duración de sesión, realmente parece que está destinado por razones de rendimiento, no para evitar la inyección a través de scripts psql. Para el acceso a psql, podría otorgar permisos a los procedimientos almacenados y vincular los parámetros dentro de los procesos.
Glenn
@Celeritas Probé el código anterior usando PostgreSQL 11.1 en x86_64 y el ejemplo SQLi anterior funcionó.
Krishna Pandey
15

Básicamente, con las declaraciones preparadas, los datos que provienen de un posible pirata informático se tratan como datos, y no hay forma de que puedan mezclarse con su aplicación SQL y / o interpretarse como SQL (lo que puede suceder cuando los datos pasados ​​se colocan directamente en su aplicación SQL).

Esto se debe a que las declaraciones preparadas "preparan" la consulta SQL primero para encontrar un plan de consulta eficiente y envían los valores reales que presumiblemente provienen de un formulario más tarde, en ese momento la consulta se ejecuta realmente.

Más información excelente aquí:

Declaraciones preparadas e inyección SQL

Jose
fuente
6

Leí las respuestas y aún sentí la necesidad de enfatizar el punto clave que ilumina la esencia de las declaraciones preparadas. Considere dos formas de consultar la base de datos de uno donde la entrada del usuario está involucrada:

Enfoque ingenuo

Uno concatena la entrada del usuario con alguna cadena parcial de SQL para generar una declaración SQL. En este caso, el usuario puede incrustar comandos SQL maliciosos, que luego se enviarán a la base de datos para su ejecución.

String SQLString = "SELECT * FROM CUSTOMERS WHERE NAME='"+userInput+"'"

Por ejemplo, la entrada de usuarios malintencionados puede llevar a SQLStringser igual a"SELECT * FROM CUSTOMERS WHERE NAME='James';DROP TABLE CUSTOMERS;'

Debido al usuario malicioso, SQLStringcontiene 2 declaraciones, donde la segunda ( "DROP TABLE CUSTOMERS") causará daños.

Declaraciones preparadas

En este caso, debido a la separación de la consulta y los datos, la entrada del usuario nunca se trata como una declaración SQL y , por lo tanto, nunca se ejecuta . Es por esta razón, que cualquier código SQL malicioso inyectado no causaría ningún daño. Por lo tanto "DROP TABLE CUSTOMERS", nunca se ejecutará en el caso anterior.

En pocas palabras, con las declaraciones preparadas, el código malicioso introducido a través de la entrada del usuario no se ejecutará.

N.Vegeta
fuente
De Verdad? ¿La respuesta aceptada no dice exactamente eso?
Su sentido común
@Su sentido común La respuesta aceptada está llena de mucha información valiosa, pero me hizo preguntarme qué implican los detalles de implementación de la separación de los datos y la consulta. Mientras que enfocarse en el punto de que los datos inyectados maliciosamente (si hubiera uno) nunca se ejecutarían golpea el clavo en la cabeza.
N.Vegeta
¿Y qué "detalles de implementación" se proporcionan en su respuesta que no se presentan allí?
Su sentido común el
si intentas ver de dónde vengo, te darás cuenta de que mi punto es el siguiente: el breve deseo de ver los detalles de implementación surgió de la necesidad de comprender la razón explícita por la cual la entrada maliciosa del usuario no causará daño. No es necesario ver los detalles de implementación. Es por eso que al darse cuenta de que los detalles de implementación fueron tales que, en ningún momento se ejecutará SQL malicioso, se enviará a casa el mensaje. Su respuesta responde a la pregunta, ¿cómo (según lo solicitado) ?, pero me imagino que otras personas (como yo) estarían satisfechas con una respuesta sucinta de por qué.
N.Vegeta
Considere esto un enriquecimiento que aclara el quid, y no como una crítica implícita (la realidad se dio cuenta de quién era el autor de la respuesta aceptada).
N.Vegeta
5

Cuando crea y envía una declaración preparada al DBMS, se almacena como la consulta SQL para su ejecución.

Posteriormente, vincula sus datos a la consulta de modo que el DBMS use esos datos como parámetros de consulta para la ejecución (parametrización). El DBMS no utiliza los datos que vincula como complemento de la consulta SQL ya compilada; son simplemente los datos.

Esto significa que es fundamentalmente imposible realizar la inyección de SQL utilizando declaraciones preparadas. La naturaleza misma de las declaraciones preparadas y su relación con el DBMS lo impide.

wulfgarpro
fuente
4

En SQL Server , el uso de una declaración preparada definitivamente es a prueba de inyección porque los parámetros de entrada no forman la consulta. Significa que la consulta ejecutada no es una consulta dinámica. Ejemplo de una declaración vulnerable de inyección SQL.

string sqlquery = "select * from table where username='" + inputusername +"' and password='" + pass + "'";

Ahora, si el valor en la variable inoutusername es algo así como 'o 1 = 1 -, esta consulta ahora se convierte en:

select * from table where username='a' or 1=1 -- and password=asda

Y el resto se comenta después --, por lo que nunca se ejecuta y se omite al usar el ejemplo de la declaración preparada como se muestra a continuación.

Sqlcommand command = new sqlcommand("select * from table where username = @userinput and password=@pass");
command.Parameters.Add(new SqlParameter("@userinput", 100));
command.Parameters.Add(new SqlParameter("@pass", 100));
command.prepare();

Entonces, en efecto, no puede enviar otro parámetro, evitando así la inyección de SQL ...

lloydom
fuente
3

La frase clave es need not be correctly escaped . Eso significa que no debe preocuparse por las personas que intentan escribir guiones, apóstrofes, citas, etc.

Todo se maneja por ti.

Feisty Mango
fuente
2
ResultSet rs = statement.executeQuery("select * from foo where value = " + httpRequest.getParameter("filter");

Supongamos que tiene eso en un Servlet que tiene razón. Si una persona malévola pasó un valor malo para 'filtro', podría hackear su base de datos.

MeBigFatGuy
fuente
0

Causa raíz # 1 - El problema del delimitador

La inyección SQL es posible porque usamos comillas para delimitar cadenas y también para ser partes de cadenas, lo que hace imposible interpretarlas a veces. Si tuviéramos delimitadores que no pudieran usarse en datos de cadena, la inyección de sql nunca habría sucedido. Resolver el problema del delimitador elimina el problema de inyección sql. Las consultas de estructura hacen eso.

Causa raíz # 2: la naturaleza humana, las personas son astutas y algunas personas astutas son maliciosas y todas las personas cometen errores

La otra causa raíz de la inyección de sql es la naturaleza humana. Las personas, incluidos los programadores, cometen errores. Cuando comete un error en una consulta estructurada, su sistema no es vulnerable a la inyección SQL. Si no está utilizando consultas estructuradas, los errores pueden generar vulnerabilidad de inyección SQL.

Cómo las consultas estructuradas resuelven las causas raíz de la inyección SQL

Las consultas estructuradas resuelven el problema del delimitador colocando comandos sql en una instrucción y colocando los datos en una instrucción de programación separada. Las declaraciones de programación crean la separación necesaria.

Las consultas estructuradas ayudan a evitar que los errores humanos creen agujeros de seguridad críticos. Con respecto a los humanos que cometen errores, la inyección de sql no puede ocurrir cuando se utilizan consultas de estructura. Hay formas de prevenir la inyección de sql que no implican consultas estructuradas, pero el error humano normal en esos enfoques generalmente conduce a al menos una cierta exposición a la inyección de sql. Las consultas estructuradas son a prueba de fallas de la inyección SQL. Puede cometer todos los errores del mundo, casi, con consultas estructuradas, igual que cualquier otra programación, pero ninguna de las que pueda hacer puede convertirse en un sistema asumido por inyección SQL. Es por eso que a la gente le gusta decir que esta es la forma correcta de prevenir la inyección de sql.

Entonces, ahí lo tiene, las causas de la inyección de SQL y las consultas estructuradas de la naturaleza que las hacen imposibles cuando se usan.

DanAllen
fuente