W3docs

Java JDBC PreparedStatement

Ejecuta SQL parametrizado de forma segura en Java con PreparedStatement para prevenir la inyección SQL.

Un PreparedStatement es una plantilla SQL con marcadores de posición ? donde van los valores. Los valores se establecen por separado mediante índice, y el controlador envía la plantilla y los datos por canales distintos — así un valor nunca puede ser interpretado como SQL. Este es el hábito más importante en JDBC: hace que la inyección sea estructuralmente imposible y permite que la base de datos reutilice el plan de consulta. Prefiere esto sobre un Statement simple para prácticamente todo.

Este capítulo explica cómo crear, vincular y ejecutar un PreparedStatement, por qué detiene la inyección SQL, cómo vincular valores NULL y tipados, y cuándo conviene reutilizar uno.

Crear, vincular, ejecutar

String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, pw);
     PreparedStatement ps = conn.prepareStatement(sql)) {
  ps.setString(1, name);   // bind by 1-based index
  ps.setInt(2, age);
  int rows = ps.executeUpdate();
}

Los marcadores de posición se numeran a partir de 1, no de 0 — una fuente constante de errores de desfase por uno. Cada setXxx coincide con el tipo de la columna: setString, setInt, setBigDecimal, setTimestamp, etc.

Por qué evita la inyección

Con un Statement, el valor forma parte del texto SQL, por lo que una comilla en el valor puede terminar el literal e inyectar nuevos comandos. Con un PreparedStatement, el SQL es fijo y se analiza antes de vincular cualquier valor; el valor se transmite luego como un parámetro tipado. No existe ninguna cadena de la que las comillas de un atacante puedan escapar — el valor peligroso del capítulo sobre Statement simplemente se convierte en un nombre literal.

Tras ejecutar una consulta, sus filas se leen con un ResultSet, exactamente igual que con un Statement.

Vincular NULL y tipos especiales

No se puede pasar un null de Java a setInt (recibe un primitivo), y setString(i, null) es ambiguo para algunos tipos. La forma explícita es setNull(index, sqlType), indicando el java.sql.Types de la columna:

ps.setNull(3, java.sql.Types.VARCHAR);

Reutilizar un prepared statement

Un prepared statement está diseñado para ejecutarse muchas veces con valores diferentes — vincular, ejecutar, limpiar, repetir. La base de datos analiza y planifica el SQL una sola vez y lo reutiliza, razón por la cual los prepared statements también son más rápidos en un bucle que reconstruir una cadena Statement cada vez. Para inserciones masivas, combina esto con el procesamiento por lotes.

Un ejemplo completo: anatomía de una plantilla y sus vínculos

Este programa trata la plantilla SQL como datos: cuenta los marcadores de posición, recorre los valores que se vincularían a ellos (incluida la cadena maliciosa que quebró el Statement) y muestra el código de tipo de setNull — todas las partes móviles del enlace de parámetros, sin necesidad de una base de datos activa.

java— editable, runs on the server

Lo que hay que extraer de la ejecución:

  • La plantilla tiene tres marcadores de posición ?, y el programa los cuenta — ese número es exactamente cuántas llamadas setXxx debes hacer. Una discrepancia (vincular el parámetro 4 de una consulta con 3 marcadores) lanza una excepción en la ejecución.
  • Los vínculos son base-1: el parámetro 1 es el primer ?. El bucle imprime bind 1, bind 2, bind 3 para enfatizarlo — el error más común de los principiantes es empezar desde 0.
  • El primer valor es la misma cadena x'; DROP TABLE users;-- que secuestró el Statement en el capítulo anterior. Aquí es simplemente un dato vinculado al parámetro 1; el controlador lo almacena literalmente como nombre. La inyección queda neutralizada por construcción, no mediante escape.
  • El null en el parámetro 3 es la razón por la que existe setNull(index, Types.VARCHAR). JDBC necesita el tipo SQL para indicar a la base de datos qué tipo de NULL es — se especifica con una constante de java.sql.Types.
  • Cada valor lleva un tipo implícito — String, int, NULL-de-VARCHAR — razón por la que existe un setXxx por tipo en lugar de un único setter de cadena. Hacer coincidir el setter con el tipo de columna es la disciplina que mantiene los prepared statements seguros y correctos.

Práctica

Práctica
¿Por qué un PreparedStatement previene la inyección SQL incluso cuando un valor vinculado contiene una comilla simple y un punto y coma?
¿Por qué un PreparedStatement previene la inyección SQL incluso cuando un valor vinculado contiene una comilla simple y un punto y coma?
Was this page helpful?