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.
- SELECT * FROM x — recorrido del árbol para verificar que no hay operaciones destructivas anidadas
- 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)