Encontrar y Corregir Consultas Inseguras a Bases de Datos

SQLiteBeginner
Practicar Ahora

Introducción

Un nombre proporcionado por un usuario debe seguir siendo un valor, sin convertirse en parte de un comando de base de datos. Esta búsqueda de DeskLite construye actualmente texto SQL directamente a partir de un nombre. Las solicitudes normales parecen funcionar, pero los nombres con comillas y las entradas con formas especiales revelan el problema.

Solo leerás una función de Python de cinco líneas, aprenderás la pequeña parte de SQL que utiliza, seguirás un hallazgo real de Semgrep y aplicarás un patrón de vinculación de parámetros de SQLite. No se requieren conocimientos previos de Python ni de SQL. El ejecutor, la base de datos, el analizador y la comprobación funcional ya están preparados.

Ejecutar una Búsqueda Normal

En este paso establecerás qué debe devolver la búsqueda antes de leer su implementación.

DeskLite almacena tres tickets ficticios en una pequeña base de datos SQLite. SQLite es un motor de base de datos que lee y consulta un archivo local. El curso ya ha preparado la conexión y los datos.

cd /home/labex/project/ticket-queries

run_lookup.py es un punto de entrada preparado. Como en el laboratorio de dependencias, python3 ejecuta un programa suministrado. El último argumento es el nombre que se buscará:

python3 run_lookup.py Alice

Espera un ticket con el ID 1 y el título Printer offline. El ejecutor da formato a las filas de la base de datos para producir la respuesta JSON conocida. Editarás la pequeña función de búsqueda, no el código de formato del ejecutor.

El límite previsto es sencillo: el nombre proporcionado selecciona los tickets de ese propietario. No debe cambiar el significado del comando de base de datos ni revelar tickets de otros propietarios. Todos los nombres y registros de este ejercicio son ficticios; trabaja únicamente con esta base de datos local preparada.

Leer la Función de Cinco Líneas

En este paso seguirás el recorrido del nombre proporcionado a través de una pequeña función de Python.

cat lookup.py

El archivo contiene:

from fixture import db

def lookup(name):
    query = f"SELECT id, title FROM tickets WHERE owner = '{name}'"
    return db.execute(query).fetchall()

from fixture import db hace que la conexión de base de datos preparada esté disponible con el nombre db. Una función es una parte de comportamiento reutilizable que tiene un nombre. def lookup(name): define una función cuya entrada se llama name. Las líneas con sangría pertenecen a esa función; conserva su sangría al editar.

Una variable da nombre a un valor. query = ... asigna un valor de texto, llamado cadena, a query. La f inicial convierte esto en una f-string: Python inserta el valor actual de name en {name}. Si la entrada es Alice, ese texto pasa a formar parte de la cadena SQL.

db.execute(query) es una llamada a un método, similar a una función, que pide a la conexión que ejecute la consulta. .fetchall() recopila sus filas de resultado. return devuelve esas filas a quien llamó a la función. El ejecutor preparado pasa tu argumento del terminal a lookup y muestra las filas devueltas.

Solo necesitas este recorrido de datos: nombre proporcionado → cadena de consulta → llamada a la base de datos → filas devueltas. A continuación leerás qué significa esa cadena SQL.

Leer la Condición SQL Prevista

En este paso relacionarás la condición SQL con los registros que deben devolverse.

SQL es un lenguaje para solicitar operaciones de base de datos. Esta consulta usa solo una pequeña parte de él:

SELECT id, title FROM tickets WHERE owner = 'Alice'

SELECT id, title elige los dos campos de resultado. FROM tickets elige la tabla preparada. WHERE owner = 'Alice' limita los resultados a las filas cuyo propietario es igual a ese valor de texto. En SQL, las comillas simples delimitan un literal de texto. El = dentro de esta cadena SQL expresa la condición del propietario. La asignación de Python query = ..., fuera de la cadena, da un nombre de variable al texto completo.

Prueba otro nombre normal:

python3 run_lookup.py Bob

Espera únicamente el ticket con el ID 3, titulado Spare keyboard. El nombre cambia el valor previsto de la condición del propietario, no las columnas seleccionadas ni la tabla.

Ahora relaciona esto con la f-string que leíste. Python inserta el nombre proporcionado entre las comillas SQL antes de que SQLite vea la consulta. Esto combina dos cosas diferentes: la estructura del comando y los datos de entrada. Una comilla dentro de un nombre real puede interferir con esos límites.

Aún no edites nada. Primero usarás el analizador fijo para localizar la construcción arriesgada y después confirmarás el comportamiento con un nombre legítimo que contiene una comilla y una entrada de prueba local autorizada.

Seguir el Hallazgo del Análisis Estático

En este paso leerás un hallazgo real de Semgrep y relacionarás su ubicación con la construcción de la consulta.

Las pruebas estáticas de seguridad de aplicaciones (SAST) examinan el código sin ejecutar todas las entradas posibles. El lanzador preparado course-code-scan usa el motor Semgrep CE 1.145.0 con versión fijada y una regla local fija. No hay ninguna cuenta en la nube ni un conjunto de reglas en línea.

course-code-scan lookup.py

Espera un hallazgo en lookup.py:5, con el ID de regla desklite.sql-parameter-binding y esta explicación:

Bind the supplied lookup name as a SQL parameter instead of building SQL text from it.

La línea señala el uso de query en la llamada a la base de datos. Sigue ese valor una línea hacia arriba para ver dónde se construyó la f-string. Es una breve investigación del flujo de datos, no una petición de desarrollar una aplicación completa.

Un hallazgo termina con 1; CHECK ERROR termina con 2 cuando no se puede analizar el archivo. Ninguno de los dos es un resultado limpio. La regla local sigue el name proporcionado de este curso hasta el texto SQL. Su alcance es limitado: un hallazgo sigue necesitando una revisión del código, y la ausencia de hallazgos con una sola regla no demuestra que una aplicación carezca de otros riesgos.

A continuación compararás el hallazgo con el comportamiento real de la búsqueda.

Comparar el Texto con Comillas y la Entrada que Altera la Consulta

En este paso observarás por qué la entrada debe seguir siendo datos, usando únicamente los datos de prueba locales preparados.

Un propietario legítimo se llama O'Neil. Las comillas dobles mantienen unido todo ese argumento del terminal, incluido su apóstrofo:

python3 run_lookup.py "O'Neil"

Espera una línea que empiece por Lookup failed: con un error de sintaxis de SQLite. Este fallo es una prueba intencionada del defecto. El apóstrofo del nombre cerró un literal de texto SQL porque el programa lo insertó directamente en el texto del comando. Rechazar todos los apóstrofos impediría la búsqueda de un propietario real.

Ahora prueba esta entrada de prueba de alcance limitado:

python3 run_lookup.py "' OR 1=1 --"

La versión insegura devuelve los tres tickets en lugar de no encontrar ningún propietario coincidente. En este contexto SQL, la comilla cierra el literal, OR 1=1 hace que la condición sea verdadera y -- convierte el texto restante en un comentario. La entrada cambió la estructura del comando en lugar de seguir siendo un nombre. Esto es inyección SQL.

No estás probando un servicio externo ni cambiando datos. La base de datos preparada es de solo lectura y contiene registros ficticios. La corrección requerida debe satisfacer ambas observaciones: un nombre legítimo con una comilla funciona, mientras que el texto que altera la consulta no puede ampliar las filas devueltas.

Vincular el Nombre por Separado del Texto SQL

En este paso corregirás la búsqueda usando la interfaz de vinculación de parámetros de SQLite.

Un marcador de posición indica dónde corresponde un valor de datos. El marcador ? de SQLite forma parte de una estructura SQL fija; el nombre se proporciona por separado a execute. El controlador de base de datos trata el valor proporcionado como datos, aunque contenga comillas o caracteres que parezcan SQL.

nano lookup.py

Reemplaza las líneas de consulta y ejecución de la función por este patrón admitido, conservando la importación, el nombre de la función y la sangría. Guarda con Ctrl+O, Enter, Ctrl+X:

from fixture import db

def lookup(name):
    query = "SELECT id, title FROM tickets WHERE owner = ?"
    return db.execute(query, (name,)).fetchall()

No pongas comillas SQL alrededor de ?. El controlador se encarga de la representación del valor. (name,) es una tupla de un elemento, una colección ordenada de valores. La coma final la convierte en una tupla; su único valor corresponde al único marcador de posición. El segundo argumento de execute transporta esa tupla por separado del texto de la consulta.

Ejecuta una búsqueda normal:

python3 run_lookup.py Alice

Después vuelve a probar el nombre legítimo que contiene una comilla:

python3 run_lookup.py "O'Neil"

Espera el ticket con el ID 1 para Alice y el ticket con el ID 2, titulado Login issue, para O'Neil. Ambos usan la misma estructura SQL fija. Si Python informa de un error de sangría o sintaxis, restaura la breve función mostrada arriba; no modifiques la base de datos ni el ejecutor.

Verificar el Rechazo y el Comportamiento Legítimo

En este paso confirmarás que la corrección conserva los nombres válidos e impide que el texto proporcionado amplíe la búsqueda.

Repite la entrada que altera la consulta:

python3 run_lookup.py "' OR 1=1 --"

Ahora espera una lista tickets vacía. SQLite busca un propietario cuyo nombre sea literalmente ese texto, y no existe ninguno. No se añadió ninguna estructura a la consulta.

Comprueba un propietario normal que no existe:

python3 run_lookup.py "Missing owner"

Espera otra lista vacía. Que un valor no exista es diferente de que una aplicación esté averiada: los nombres normales y los legítimos con comillas siguen devolviendo sus propios tickets.

Ejecuta la comprobación funcional de regresión proporcionada usando el mismo estilo de entrada del laboratorio de dependencias:

bash test.sh

Espera:

PASS: ordinary, quoted, missing, and injection-like names.

Estas comprobaciones prueban llamadas reales a SQLite y filas de resultado. Eliminar la función, devolver una lista vacía para todas las entradas o borrar datos no conservaría los resultados legítimos. Una lista de caracteres prohibidos tampoco es suficiente: un propietario real tiene un apóstrofo. La defensa prevista es separar el texto SQL fijo de los valores vinculados.

Volver a Analizar el Código Corregido

En este paso combinarás el resultado estático con el comportamiento real de búsqueda que acabas de probar.

course-code-scan lookup.py

Espera No findings under the fixed local rule. El nombre proporcionado ya no pasa a formar parte de la cadena del comando SQL. Se transmite como un valor vinculado por separado.

Vuelve a ejecutar la comprobación funcional:

bash test.sh

Espera el mismo resultado satisfactorio. El análisis y la prueba de comportamiento responden a preguntas diferentes: uno examina el patrón de código objetivo; la otra comprueba que los nombres reales siguen funcionando y que una entrada especial no puede ampliar el resultado.

La verificación del curso también observa los argumentos reales de SQLite, por lo que limitarse a ocultar un hallazgo del analizador o reemplazar manualmente las comillas no es la corrección requerida. Conserva la base de datos, los datos de prueba, el ejecutor y la interfaz de la función originales. No se necesita un archivo de investigación escrito.

Este laboratorio enseñó un pequeño patrón de vinculación de SQLite y una regla local de SAST. Una revisión completa de la aplicación también tendría en cuenta otras fuentes de entrada y clases de vulnerabilidades. Aquí has eliminado el riesgo específico de que la entrada pase a formar parte de la consulta y has conservado la búsqueda prevista.

Resumen

Seguiste el recorrido de un nombre proporcionado a través de una breve búsqueda de Python y una condición SQL, relacionaste un hallazgo local real de Semgrep con el comportamiento en ejecución y usaste la vinculación de parámetros de SQLite. Los nombres normales y con comillas siguen funcionando, mientras que las entradas que alteran la consulta no pueden ampliar los resultados. Las pruebas estáticas y funcionales respaldan el límite específico corregido.