web-hacking

Hibernate Query Language: Pentest, HQL Injection e Java

Hibernate Query Language: Pentest, HQL Injection e Java

Cos’è Hibernate Query Language? Significato, query Hibernate, esempi Java, SQL vs HQL, come individuare vulnerabilità HQL Injection durante un pentesting.

  • Pubblicato il 2026-10-29
  • Tempo di lettura: 12 min

HQL Injection: come sfruttare query Hibernate costruite male #

HQL, acronimo di Hibernate Query Language, è il linguaggio di interrogazione usato da Hibernate — la libreria Java più diffusa per parlare con i database senza scrivere SQL a mano. Quando l’input di un utente finisce dentro una query HQL per concatenazione (incollato come testo, invece di essere passato come dato separato), nasce la HQL injection: una vulnerabilità che permette di modificare la logica della query stessa, non solo il suo risultato.

Per capirla da zero: ogni volta che un sito o un’app ti chiede un username e una password, da qualche parte nel codice c’è una “domanda” fatta al database: “esiste un utente con queste credenziali?”. Quella domanda si chiama query. Quando un programma costruisce quella domanda incollando ciò che scrivi tu dentro il testo della query, invece di tenerlo separato, può succedere che tu riesca a modificare la domanda stessa — non solo a rispondere, ma a cambiare cosa viene chiesto al database. Questo è il principio dietro ogni injection, inclusa la HQL injection.

Hibernate — una libreria Java che fa da intermediario tra il codice dell’applicazione e il database (questo intermediario si chiama ORM, Object-Relational Mapping) — usa HQL per costruire quelle domande internamente. Il problema di sicurezza che vediamo in questo articolo è esattamente lo stesso principio della SQL injection classica, applicato a HQL — ma con alcune differenze operative che vale la pena conoscere prima di testare qualcosa, anche se non hai mai sentito parlare né di ORM né di Hibernate prima di oggi.

Cos’è HQL (Hibernate Query Language) #

Immagina un database come un grande archivio di scatole (le tabelle), ognuna con etichette (le colonne) e dentro dei foglietti (le righe di dati). SQL è il linguaggio che parla direttamente con quell’archivio in questi termini: “scatola, etichetta, foglietto”.

Hibernate invece fa parlare il programmatore Java in un altro modo: non in termini di scatole e foglietti, ma di oggetti del linguaggio Java (User, Product, Order — le classi che il programmatore ha scritto per rappresentare un utente, un prodotto, un ordine). HQL è il linguaggio con cui si interrogano questi oggetti. Dietro le quinte, Hibernate traduce ogni query HQL in SQL vero e proprio al momento dell’esecuzione, adattandola al database realmente usato (MySQL, PostgreSQL, Oracle, SQL Server…). Per questo la stessa query HQL funziona senza modifiche su database diversi — è Hibernate che si occupa della traduzione.

Una query HQL minima:

java
Query query = session.createQuery("FROM User WHERE username = :username");
query.setParameter("username", "admin");
List<User> results = query.list();

HQL vs SQL: le differenze principali #

Chi arriva da un background SQL classico deve tenere a mente alcune differenze prima di ragionare in termini di query Hibernate:

  • Oggetti, non tabelle. HQL interroga entità Java mappate via annotazioni (@Entity) o file XML, non tabelle fisiche. FROM User restituisce istanze dell’entità User, non righe grezze.
  • Portabilità. La stessa query HQL gira su MySQL, PostgreSQL, Oracle o SQL Server senza modifiche: è Hibernate a generare l’SQL nativo corretto per il dialetto configurato.
  • Join impliciti sulle relazioni mappate. Le associazioni (@OneToMany, @ManyToOne) sono navigabili direttamente nella query (FROM Order o WHERE o.customer.name = 'Mario'), senza scrivere JOIN espliciti.
  • Nessuna sintassi DDL. HQL copre SELECT, UPDATE, DELETE sulle entità, ma non CREATE/ALTER — per quello Hibernate usa la configurazione di mapping, non query.
  • UNION dipende dalla versione. Fino a Hibernate 5.x, UNION non è supportato in HQL. Da Hibernate ORM 6 in poi (alignment con JPQL 3.1+) UNION, INTERSECT ed EXCEPT sono disponibili — un dettaglio che, come vedremo, cambia completamente l’approccio a una injection.

Quando la query deve usare funzioni specifiche del database non astratte da HQL, Hibernate offre anche le native query:

java
Query nativeQuery = session.createNativeQuery("SELECT * FROM users WHERE username = :username");
nativeQuery.setParameter("username", "admin");

Una native query è SQL puro: se concatenata con input utente, il rischio diventa SQL injection “tradizionale”, non HQL injection — distinzione importante quando fai bug hunting su un’app Hibernate, perché le due superfici si testano diversamente.

Il codice vulnerabile #

Il pattern tipico, visto in centinaia di applicazioni Spring/Hibernate:

java
// VULNERABLE: user input concatenated directly into HQL
String hql = "FROM User WHERE username = '" + username + "' AND password = '" + password + "'";
Query query = session.createQuery(hql);
List<User> results = query.list();

Se username arriva da una request HTTP senza binding dei parametri, puoi chiudere la stringa ed iniettare logica HQL arbitraria, allo stesso modo in cui faresti con una query SQL concatenata male.

Tecniche operative #

1. Bypass di autenticazione (error-based / boolean) #

Il classico payload di bypass, copiato pari pari dalla SQL injection, qui va verificato con attenzione perché il risultato dipende dalla query esatta e dalla precedenza tra AND e OR:

text
username=admin' OR '1'='1

Sostituito nella query vulnerabile vista sopra, produce:

sql
FROM User WHERE username = 'admin' OR '1'='1' AND password = '...'

Qui serve attenzione: in SQL e in HQL AND ha precedenza su OR, quindi questa espressione si valuta come username = 'admin' OR ('1'='1' AND password = '...'). Se la password è sbagliata, la seconda metà è falsa — ma la prima (username = 'admin') può già bastare a far restituire una riga, a seconda di come il codice Java gestisce il risultato (se prende il primo risultato della lista senza controllare la password a parte). In pratica il bypass funziona quando la logica applicativa si fida ciecamente del fatto che la query abbia restituito qualcosa, non perché la condizione OR '1'='1' renda automaticamente vero l’intero controllo — va sempre verificato in laboratorio contro la query reale, non assunto per analogia con la SQL injection.

2. Boolean-based blind #

Senza UNION (su Hibernate < 6) e senza una SLEEP() nativa, il blind si lavora quasi sempre in modalità boolean-based, osservando la differenza di comportamento dell’applicazione (200 vs 403, contenuto diverso, redirect diverso):

text
' AND (SELECT COUNT(u) FROM User u WHERE u.username='admin') > 0 AND '1'='1
' AND (SELECT COUNT(u) FROM User u WHERE u.username='admin') > 0 AND '1'='2

Confrontando le due risposte isoli il comportamento “vero/falso” e puoi enumerare carattere per carattere con SUBSTRING, una funzione standard sia in HQL che in JPQL:

text
' AND SUBSTRING((SELECT u.password FROM User u WHERE u.username='admin'), 1, 1) = 'a

Attenzione: una subquery scalare dentro una condizione booleana non è garantita su ogni versione di Hibernate con ogni grammatica — testala prima in un ambiente che replica il target (stessa versione di Hibernate, stesso dialetto configurato) prima di darla per scontata, e tieni a mente che una subquery che restituisce più di una riga genera un errore invece di un booleano puro.

3. Time-based “simulato” via cartesian product (tecnica non affidabile) #

Senza SLEEP(), un’idea circolata in alcuni writeup è forzare Hibernate a generare un prodotto cartesiano pesante tramite una subquery senza condizione di join:

text
' AND (SELECT COUNT(a) FROM User a, User b, User c) > 0 AND '1'='1

Da trattare con cautela, non come tecnica garantita: il query planner del database può ottimizzare il prodotto cartesiano in modi non prevedibili, il delay introdotto non è costante e su una tabella grande rischi di causare un carico eccessivo sul server (di fatto un mini-DoS, non qualcosa da lanciare su un sistema in produzione). Usala solo in laboratorio, con un dataset controllato e limiti di esecuzione, e considerala un’ultima spiaggia quando il boolean-based non è osservabile — non un oracolo temporale affidabile come SLEEP() in SQL.

4. Estrazione cross-entity (il vantaggio reale di HQL) #

Questa è la tecnica che distingue davvero l’HQL injection dalla SQL injection. Supponi che l’app abbia un’entità User (quella del parametro vulnerabile) e un’entità scorrelata ApiCredential con un campo secretKey. In SQL ti servirebbe un UNION SELECT o un join esplicito. In HQL, su versioni precedenti alla 6, puoi arrivarci con una subquery scalare dentro la condizione boolean-based, senza alcun bisogno di UNION:

text
' AND (SELECT COUNT(c) FROM ApiCredential c WHERE c.secretKey LIKE 'a%') > 0 AND '1'='1

Ripetendo il boolean-based su questa subquery estrai, carattere per carattere, dati da un’entità che il parametro iniettabile non avrebbe mai dovuto poter raggiungere — l’unico vincolo è che l’entità sia mappata da Hibernate nello stesso SessionFactory, non che sia collegata logicamente a User.

Tecniche avanzate per DBMS specifico #

Le tecniche precedenti funzionano a prescindere dal database sottostante. Esistono però trucchi più avanzati, documentati da ricerche pubbliche (Sonar, Trustwave SpiderLabs, Egorov & Soldatov su HITB), che sfruttano differenze specifiche tra come HQL interpreta certi caratteri e come li interpreta il database vero e proprio. Sono tecniche più delicate, ma permettono di “uscire” dal recinto limitato di HQL e parlare quasi direttamente con SQL nativo.

HQL non ha commenti #

Prima cosa da sapere: a differenza di SQL, HQL non supporta i commenti (-- o #). Questo significa che non puoi “tagliare via” il resto della query con un commento come fai in SQL injection classica — ogni payload deve restare sintatticamente valido fino alla fine della query originale, chiudendo parentesi e apici in modo corretto.

Scoprire le classi (entità) mappate via messaggi d’errore #

In SQL, quando non conosci lo schema del database, enumeri tabelle e colonne tramite le tabelle di sistema (information_schema e simili). In HQL questa strada non esiste: non ci sono metadati di sistema interrogabili, perché HQL lavora solo sulle classi Java che Hibernate ha mappato come entità. Il modo per scoprire quali classi esistono è quindi diverso: sfrutti il fatto che Hibernate, quando un’entità non esiste o un campo non esiste su quell’entità, restituisce un errore che nomina esattamente il problema.

Scoprire se una classe esiste. Se conosci (o indovini) il nome di una possibile entità, puoi testarla iniettando un riferimento a quella classe dentro una subquery:

text
' AND (SELECT COUNT(x) FROM ApiCredential x) >= 0 AND '1'='1
  • Se ApiCredential esiste come entità mappata, la query è valida e la risposta dell’applicazione è quella “normale” (vera).
  • Se non esiste, Hibernate lancia un’eccezione tipo QuerySyntaxException: ApiCredential is not mapped — e quel messaggio, se non è nascosto, ti dice esplicitamente che quel nome di classe non è mappato, permettendoti di scartarlo e provare il successivo.

Questo trasforma l’enumerazione in un processo di trial-and-error: provi nomi plausibili (User, Account, ApiKey, Session, Role, Permission, AuditLog — spesso ricavabili dai nomi delle risorse esposte dall’API, dai path degli endpoint, o dai nomi di classe visti in eventuali stack trace precedenti) finché non trovi quelli che Hibernate riconosce come mappati.

Scoprire i campi di una classe già nota. Una volta identificata una classe esistente, puoi enumerare le sue proprietà allo stesso modo, testando un nome di campo per volta:

text
' AND (SELECT x.secretKey FROM ApiCredential x WHERE 1=0) IS NULL AND '1'='1

Se secretKey non è una proprietà di ApiCredential, l’errore di Hibernate la nomina esplicitamente come proprietà sconosciuta sull’entità (es. could not resolve property: secretKey of: ApiCredential) — di nuovo, informazione che ottieni gratis dal messaggio d’errore, utile per costruire poi i payload di estrazione cross-entity visti nella tecnica precedente.

Nota importante: questa enumerazione funziona solo se l’applicazione mostra all’utente il messaggio d’errore originale di Hibernate (vedi la sezione difensiva più sotto sul perché va evitato). Se gli errori sono generici, questa via è chiusa e serve ragionare “alla cieca” sul modello dati partendo da ciò che l’applicazione espone pubblicamente (nomi di endpoint, campi nei form, risposte JSON).

Discrepanza di escaping su MySQL #

Questo è il trucco più noto per “evadere” da HQL verso SQL puro, descritto per la prima volta da Mikhail Egorov. La causa è semplice: HQL raddoppia l’apice per escaparlo (''), mentre MySQL usa il backslash (\'). Un payload costruito con un backslash viene letto in modo diverso dai due parser:

text
dummy\' OR 1=1 OR '

Hibernate vede una stringa che finisce dopo il backslash-apice (lo interpreta come carattere letterale, non come fine stringa) e costruisce una query sintatticamente valida dal suo punto di vista — ma quando quella stringa arriva a MySQL, il backslash chiude davvero l’apice nel punto sbagliato, e il resto del payload (OR 1=1 OR) viene eseguito come SQL vero, non come testo. Funziona solo su MySQL: PostgreSQL, Oracle e SQL Server trattano il backslash come carattere normale, quindi qui il trucco non si applica.

Funzioni “magiche” (PostgreSQL e Oracle) #

Alcuni database offrono funzioni che accettano una stringa SQL come parametro e la eseguono per davvero — Hibernate permette di chiamare funzioni con nome arbitrario dentro una query HQL, quindi se il DBMS ne offre una di queste, hai di fatto un modo per far eseguire SQL nativo.

Su PostgreSQL, query_to_xml() esegue la query passata come stringa e restituisce il risultato in formato XML — utile per un boolean-based molto affidabile:

text
dummy' AND array_upper(xpath('/row', query_to_xml('SELECT 1 WHERE 1=1', true, false, '')), 1) = 1 AND '1'='1

Su Oracle, DBMS_XMLGEN.getxml() ha lo stesso ruolo: esegue la query e restituisce un CLOB (o NULL se non ci sono righe), che puoi confrontare con NVL/TO_CHAR per ottenere un boolean-based:

text
dummy' AND NVL(TO_CHAR(DBMS_XMLGEN.getxml('SELECT 1 FROM dual WHERE 1=1')), 'x') != 'x' AND '1'='1

Bypass con spazi Unicode (SQL Server / H2) #

SQL Server e H2 trattano alcuni caratteri Unicode come lo spazio non-interrotto (U+00A0) come normali separatori tra parole chiave. Hibernate, allo stesso tempo, li accetta dentro i nomi di identificatori. Questa doppia tolleranza permette di costruire payload dove gli spazi “normali” sono sostituiti da U+00A0 (codificato come %C2%A0 in URL), aggirando filtri che bloccano solo lo spazio ASCII classico senza rompere la sintassi per nessuno dei due parser.

Identificare l’ORM prima di scegliere la tecnica #

Prima di applicare una tecnica specifica per Hibernate, conviene verificare che sia davvero Hibernate (e non un altro ORM Java come EclipseLink, TopLink o OpenJPA, che hanno sintassi e trucchi diversi). Un payload di fingerprint semplice per Hibernate:

text
' AND 1=1--

genera un comportamento diverso da quello prodotto su EclipseLink (che userebbe FUNCTION('1=1 and','2')='2') o su TopLink (SQL('1=1')) — distinguere l’ORM per nome evita di perdere tempo con la tecnica sbagliata.

RCE su MySQL con privilegio FILE #

Se l’utente con cui l’applicazione si connette al database ha il privilegio FILE e la variabile secure_file_priv non lo impedisce, una volta ottenuta l’evasione verso SQL nativo (vista sopra) puoi scrivere un file sul filesystem del server tramite SELECT ... INTO OUTFILE:

sql
SELECT '<%@ page import="java.util.*,java.io.*"%><% ... %>' INTO OUTFILE '/var/www/app/webapps/ROOT/shell.jsp'

Se quella directory è servita pubblicamente dall’application server (Tomcat, nel caso di molte app Java), il file scritto diventa una webshell raggiungibile via browser — da HQL injection a Remote Code Execution. Funziona solo su MySQL e solo con quei due prerequisiti di privilegio.

Detection e difesa (Blue Team) #

Binding dei parametri, sempre. La correzione è identica concettualmente alla SQL injection: mai concatenare, sempre parametri nominati o posizionali.

java
// SAFE: named parameter binding
String hql = "FROM User WHERE username = :username AND password = :password";
Query query = session.createQuery(hql);
query.setParameter("username", username);
query.setParameter("password", password);

Preferire Criteria API / JPA CriteriaBuilder quando la query va costruita dinamicamente in base a input variabile — elimina la necessità di manipolare stringhe HQL a runtime.

Log pattern da monitorare. A differenza della SQL injection, nei log applicativi Hibernate (se il logging delle query è attivo) i payload HQL iniettati sono visibili come clausole WHERE anomale che referenziano entità non pertinenti al controller chiamato — una query dell’endpoint di login che referenzia ApiCredential è un indicatore forte di tentativo di injection, perché quell’entità non ha alcuna ragione di apparire in quel contesto applicativo.

Il WAF non è una soluzione, al massimo un livello extra. Un WAF può comunque ispezionare la richiesta HTTP e bloccare pattern sospetti (apici, parole chiave SQL) prima che arrivino all’applicazione, anche senza capire il contesto HQL — ma l’injection avviene a livello di ORM, prima che la query raggiunga il database come SQL grezzo, quindi un firewall a livello di database non vede nulla di anomalo: la query SQL finale generata da Hibernate è sintatticamente valida. Un WAF riduce la superficie, non sostituisce il parameter binding.

Testing manuale, non sqlmap. Non esiste supporto nativo per HQL injection negli scanner automatici più comuni: la rilevazione va fatta manualmente con Burp Suite , intercettando parametri che finiscono in query Hibernate e testando payload boolean-based come sopra.

Come difendersi: guida pratica per sviluppatori e blue team #

1. Parametri nominati, sempre #

Ogni valore che arriva dall’utente entra nella query solo tramite binding. Hibernate lo passa al database come parametro, mai come testo da interpretare.

java
// SAFE: user input is bound, never concatenated
TypedQuery<User> query = entityManager.createQuery(
    "SELECT u FROM User u WHERE u.username = :username", User.class);
query.setParameter("username", username);

2. Spring Data JPA #

Con @Query il binding è lo stesso. Evita di costruire la stringa della query dentro un metodo custom del repository.

java
@Query("SELECT u FROM User u WHERE u.username = :username")
Optional<User> findByUsername(@Param("username") String username);

3. ORDER BY e nomi di campo: allowlist #

Il binding non funziona per nomi di colonne o direzione di ordinamento, ed è il punto dove la concatenazione sopravvive più spesso. Qui l’unica difesa è una lista di valori ammessi: l’input dell’utente sceglie una chiave, non finisce mai nella query.

java
// SAFE: only values from the allowlist reach the query string
private static final Map<String, String> SORTABLE_FIELDS =
    Map.of("name", "u.username", "created", "u.createdAt");

String column = SORTABLE_FIELDS.getOrDefault(sortParam, "u.username");
String direction = "desc".equalsIgnoreCase(directionParam) ? "DESC" : "ASC";

TypedQuery<User> query = entityManager.createQuery(
    "SELECT u FROM User u ORDER BY " + column + " " + direction, User.class);

4. Niente funzioni di escaping fatte in casa #

Raddoppiare gli apici a mano non è una difesa: HQL e il database sottostante interpretano apici e backslash in modo diverso, quindi una funzione che “funziona” per HQL può non bastare per MySQL o PostgreSQL. Se vedi una escape() o sanitize() dentro la costruzione di una query, trattala come vulnerabilità finché non è sostituita da un parametro.

5. Criteria API per le query dinamiche #

Quando i filtri cambiano in base all’input (ricerca avanzata, filtri opzionali), usa CriteriaBuilder invece di assemblare stringhe HQL.

java
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
criteria.select(root).where(builder.equal(root.get("username"), username));
List<User> results = entityManager.createQuery(criteria).getResultList();

6. Privilegi minimi sull’utente database #

L’utente con cui l’applicazione si collega non deve avere privilegi che non usa. Su MySQL, niente privilegio FILE e secure_file_priv impostato su una directory dedicata o disabilitato: questo chiude la scrittura di file da una injection. Un utente applicativo separato per ogni servizio limita il danno quando uno solo è compromesso.

sql
-- Check what the application account can do
SHOW GRANTS FOR 'app_user'@'%';
SELECT @@secure_file_priv;

7. Errori generici verso il client #

Le eccezioni di Hibernate rivelano nomi di classi, entità e proprietà. In produzione il client deve ricevere un messaggio generico (stesso codice HTTP, nessuno stack trace), mentre il dettaglio va solo nei log del server.

8. Trovare le query vulnerabili nel codice #

Una ricerca rapida delle query costruite per concatenazione:

bash
grep -rnE 'create(Native)?Query\([^;]*\+' --include=*.java src/

Ogni risultato va controllato a mano. Per l’automazione, regole SAST (Semgrep, CodeQL) con taint analysis seguono l’input dal controller fino a createQuery().

9. Monitoraggio #

  • Picchi di QuerySyntaxException, QueryException o SemanticException negli application log
  • Parametri con apici, backslash o caratteri Unicode insoliti sugli endpoint che interrogano il database
  • Query che referenziano entità non pertinenti al controller chiamato, se il logging delle query è attivo
  • Risposte che rallentano in modo anomalo su un singolo endpoint

10. Test automatici #

Aggiungi ai test di integrazione input ostili (', \', ' OR '1'='1) sugli endpoint di ricerca e login. L’esito atteso è zero risultati o un errore gestito, mai un’eccezione Hibernate che arriva al client.

FAQ #

Cos’è HQL (Hibernate Query Language)? È il linguaggio di query orientato agli oggetti di Hibernate: sintassi simile a SQL, ma le query lavorano su entità Java mappate, non su tabelle fisiche, e Hibernate le traduce in SQL nativo in base al database configurato.

Che differenza c’è tra SQL e HQL? SQL interroga direttamente tabelle e colonne del database; HQL interroga entità e proprietà Java, è portabile tra DBMS diversi, e supporta nativamente le relazioni mappate senza scrivere JOIN espliciti. UNION in HQL è disponibile solo da Hibernate 6 in poi.

HQL injection è la stessa cosa della SQL injection? No. Condividono il meccanismo (concatenazione di input non fidato in una query), ma HQL opera sul modello a oggetti di Hibernate: gli errori riferiscono entità e proprietà Java, non tabelle e colonne SQL, e tecniche come UNION dipendono dalla versione di Hibernate in uso.

sqlmap funziona contro HQL injection? No, sqlmap è progettato per SQL injection su query native verso il database e non riconosce la sintassi HQL. Il testing resta manuale, con Burp Suite per intercettare e modificare i parametri.

Serve un WAF per fermare l’HQL injection? Un WAF può bloccare pattern sospetti nella richiesta HTTP, ma un firewall a livello di database non vede nulla di anomalo, perché la query SQL finale generata da Hibernate è sintatticamente corretta. Nessuno dei due sostituisce la difesa vera: parameter binding a livello applicativo.

Quali applicazioni sono a rischio? Qualunque applicazione Java che usa Hibernate (direttamente o tramite Spring Data JPA) e costruisce query HQL dinamiche concatenando input utente invece di usare setParameter() o la Criteria API.


Per approfondire la famiglia di injection su ORM, vedi anche l’articolo su SQL injection su ORM , che tratta le vulnerabilità analoghe su Django ORM, Sequelize e SQLAlchemy.

#Hibernate #HQL #Java #HQL Injection #Hibernate Query Language

lascia un messaggio

Non sono un robot