AST parsing para seguridad SQL: cómo funciona pg_query bajo el capó

Cómo Vericto convierte cada query SQL en un árbol sintáctico para detectar operaciones destructivas con certeza matemática.

9 min de lectura
TÉCNICO AST PARSING

Qué es un AST y por qué importa para seguridad

Un Abstract Syntax Tree (árbol sintáctico abstracto) es la representación estructurada de un programa. Cada nodo del árbol corresponde a una construcción sintáctica: una sentencia, una expresión, un operador.

Para SQL, esto significa que una query como DELETE FROM users se convierte en un árbol con un nodo raíz DeleteStmt que tiene propiedades: la tabla afectada, la cláusula WHERE (ausente en este caso), y otros metadatos.

La diferencia con los enfoques basados en texto (regex, tokenización simple, keyword matching) es fundamental: el AST captura la semántica de la query, no solo su forma.

pg_query: el parser oficial de PostgreSQL

pg_query es un binding de libpg_query, la librería C que PostgreSQL usa internamente para parsear queries. Esto significa que el AST que Vericto construye es idéntico al que PostgreSQL construiría para ejecutar la query.

La implicación de seguridad es importante: no hay divergencia entre lo que Vericto analiza y lo que la base de datos ejecuta. No hay formas de evasión basadas en diferencias de parsing.

Ejemplo en Rust (cómo lo usa Vericto)

use pg_query::{parse, NodeMut, NodeRef};

fn has_delete_without_where(sql: &str) -> Result<bool, pg_query::Error> {
    let result = parse(sql)?;

    for stmt in result.protobuf.stmts.iter() {
        if let Some(node) = &stmt.stmt {
            if let Some(NodeRef::DeleteStmt(delete)) = node.node.as_ref().map(|n| n.into()) {
                // WhereClause es None si no hay cláusula WHERE
                if delete.where_clause.is_none() {
                    return Ok(true);
                }
            }
        }
    }

    Ok(false)
}

// Test
assert!(has_delete_without_where("DELETE FROM users").unwrap());
assert!(!has_delete_without_where("DELETE FROM users WHERE id = $1").unwrap());

El resultado del parsing es un árbol de nodos protobuf que representan cada elemento de la query.

sqlparser-rs para MySQL, SQLite y Snowflake

Para los dialectos que no son Postgres, Vericto usa sqlparser-rs (apache/datafusion-sqlparser-rs), una librería Rust con soporte para MySQL, SQLite, SQL Server, Oracle y Snowflake.

El mismo recorrido de árbol y las mismas reglas se aplican independientemente del dialecto. La capa de reglas es agnóstica al parser subyacente.

use sqlparser::dialect::MySqlDialect;
use sqlparser::parser::Parser;
use sqlparser::ast::{Statement, Expr};

fn analyze_mysql_delete(sql: &str) -> AnalysisResult {
    let dialect = MySqlDialect {};
    let ast = Parser::parse_sql(&dialect, sql).unwrap();

    for stmt in &ast {
        if let Statement::Delete { selection, limit, .. } = stmt {
            // En MySQL: LIMIT hace seguro el DELETE
            let has_limit = limit.is_some();
            let has_where = selection.is_some();

            if !has_where && !has_limit {
                return AnalysisResult::Blocked {
                    rule: "VERICTO-001",
                    node_path: "DeleteStmt > WhereClause = NULL",
                };
            }
        }
    }

    AnalysisResult::Allowed
}

Cómo Vericto recorre el árbol en profundidad

Las queries más peligrosas son las que esconden operaciones destructivas en nodos no obvios. Una query que parece un SELECT puede contener un DELETE en una CTE.

Vericto recorre el AST en profundidad (DFS), evaluando cada nodo contra el conjunto de reglas activas. El recorrido es completo — ningún subárbol queda sin evaluar.

WITH cleanup AS (
  UPDATE sessions SET status = 'expired'
  RETURNING id
)
DELETE FROM audit_log
WHERE session_id NOT IN (SELECT id FROM cleanup);

Una query puede contener múltiples operaciones. El recorrido completo garantiza que todas sean evaluadas, no solo la primera.

  1. SELECT * FROM x — recorrido del árbol para verificar que no hay operaciones destructivas anidadas
  2. WITH x AS (DELETE FROM users) SELECT * FROM x — detectado por recorrido del CTE

Si cualquier nodo viola una regla activa, la query es bloqueada. No importa en qué nivel del árbol esté el nodo infractor.

SelectStmt
└── withClause: CommonTableExpr
    ├── ctename: "cleanup"
    └── ctequery: UpdateStmt   ← nodo infractor encontrado aquí
        ├── relation: "sessions"
        ├── targetList: [ResTarget(status = 'expired')]
        └── whereClause: NULL  ← VERICTO-042 activado

DeleteStmt
├── relation: "audit_log"
└── whereClause: NOT IN (SubLink)  ← seguro, pero la query entera se bloquea

Por qué <2ms es alcanzable

El parsing AST puede sonar computacionalmente costoso. En la práctica, para queries típicas (<500 tokens), el parsing con pg_query tarda entre 0.1ms y 0.4ms en hardware moderno.

El recorrido del árbol y la evaluación de reglas añaden otros 0.1-0.2ms. El total del proceso de análisis raramente supera 0.5ms.

  • El parser está implementado en Rust — sin GC, sin overhead de runtime
  • El análisis ocurre completamente en memoria — sin I/O
  • El registro en el audit trail es asíncrono — no bloquea el path crítico

Benchmarks con 10M queries en Postgres (Apple M2, 16GB RAM):

Benchmarking ast_parse/simple_select
  time: [312 µs 318 µs 325 µs]

Benchmarking ast_parse/complex_cte_with_subquery
  time: [847 µs 863 µs 881 µs]

Benchmarking ast_parse/update_without_where
  time: [298 µs 304 µs 311 µs]

Benchmarking ast_parse/rule_evaluation_vericto001
  time: [41 µs 43 µs 45 µs]

El overhead del proxy en el p99 es de 1.3ms. Para el 95% de las queries, el overhead es imperceptible (<0.5ms).

Las reglas como predicados sobre el AST

El engine de reglas de Vericto evalúa predicados sobre el AST. Cada regla es una función que toma un StatementInfo (la representación normalizada del nodo) y devuelve true o false.

Este diseño hace que las reglas sean determinísticas por construcción: el mismo input siempre produce el mismo resultado. No hay estado, no hay randomness, no hay dependencia de contexto externo.

El motor AST de Vericto para las 20 reglas estándar es open-source bajo licencia MIT. Puedes inspeccionar exactamente qué se evalúa y cómo.

Limitaciones honestas del AST parsing

El AST parsing es determinístico y tiene cero falsos positivos para las reglas implementadas. Pero tiene limitaciones honestas que vale la pena documentar.

  • No entiende el contexto de negocio — no puede saber si un DELETE específico es intencionado o accidental
  • No tiene información sobre el volumen de datos — no puede saber si un SELECT sin LIMIT devolverá 10 o 10M filas
  • No detecta inyección de SQL que llegue ya pre-parseada en parámetros (eso requiere análisis de parámetros en runtime)