Java: EasyBuggy - 30分でわかる！SQLインジェクションの脆弱性

# 概要

[EasyBuggy](https://github.com/k-tamura/easybuggy/blob/master/README.jp.md)と[EasyBuggy Boot](https://github.com/k-tamura/easybuggy4sb/blob/master/README.jp.md)を利用して、SQLインジェクションの脆弱性をシミュレートし、その修正を試みます。

環境構築はこちらの記事から。

https://qiita.com/flyaway/items/4922af5dbaafa1942e88

## 注意事項

本記事では脆弱性をついた攻撃手法について解説しています。

しかし、実際に第三者が運用しているWebサービスなどに対して、勝手に脆弱性の検査をおこなうのは違法行為となる可能性が非常に高いです。無害な検査用文字列を送信しているだけのつもりであっても、意図しない破壊を招いたり、監視システムによって攻撃と勘違いされる可能性があります。

絶対にやめましょう。

参考事例: [脆弱性検査について](https://www.whitehackerz.jp/blog/%E8%84%86%E5%BC%B1%E6%80%A7%E6%A4%9C%E6%9F%BB%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/)

# SQLインジェクションの怖さ

SQLインジェクションの怖さを理解するには、下記の動画がおすすめです。

<p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Vvgmeu128ak" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</p>

これは、実際に経済産業省からも注意喚起がされている脆弱性ですね。

https://www.meti.go.jp/press/2019/12/20191220013/20191220013.html

# 脆弱性を確認する

EasyBuggyを起動し、`脆弱性 > SQLインジェクション`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571/e4652f63-2892-ffe3-e840-0a05a531a9ca.png)

画面の指示に従い、パスワードに `' OR '1'='1` を入力します。
すると、ほかのユーザーすべての情報が表示できてしまいます。

# 脆弱性を修正する

XSS脆弱性の根本的解決のためには、いくつかの方法があります。
そのうち、今回は「SQL文の組み立ては全てプレースホルダで実装する」という方法での修正を試みます。

https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_1.html

ただ、上記のページの文章を読むだけでは、ちょっと意味がつかみにくいかと思います。
実際のコードで見てみましょう。

## EasyBuggyの場合

SQLInjectionServletクラスに問題となる実装があります。

```java:SQLInjectionServlet
import java.sql.Statement;
// (中略)
Statement stmt = null;
// (中略)
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT name, secret FROM users WHERE ispublic = 'true' AND name='" + name
        + "' AND password='" + password + "'");
```

### SQLインジェクションの原因

さきほど確認したSQLインジェクションの原因ですが、このSQL文にパスワードを渡すときの実装に問題があります。
プログラムが実行されると、下記のようなSQL文が実行されます。

```sql
SELECT name, secret FROM users WHERE ispublic = 'true' AND name='[入力された名前]' AND password='[入力されたパスワード]'
```

このSQL文の`[入力した名前]`を`Mark`、`[入力されたパスワード]`を`' OR '1'='1`で置き換えてみます。

```sql
SELECT name, secret FROM users WHERE ispublic = 'true' AND name='Mark' AND password='' OR '1'='1'
```

[SQLの演算子はANDが先に処理](https://qiita.com/nullponn/items/5e51d89c918a4668a974)されます。
このため、WHERE句の`ispublic = 'true' AND name='Mark' AND password=''`の部分が先に処理されます。
`name='Mark' AND password=''` を満たすレコードはないため、結果は`FALSE`となります。

```sql
SELECT name, secret FROM users WHERE FALSE OR '1'='1'
```

`'1'='1'` の結果は `TRUE` です。

```sql
SELECT name, secret FROM users WHERE FALSE OR TRUE
```

`FALSE OR TRUE` の結果は `TRUE` です。


```sql
SELECT name, secret FROM users WHERE TRUE
```

つまり、すべてのレコード行を返すSQL文になってしまいました。

順を追って修正していきましょう。


### 1. Statement から PreparedStatementへの変更

SQL文を組み立てるときは、[java.sql.Statement](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/Statement.html)ではなく[java.sql.PreparedStatement](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/PreparedStatement.html)を使いましょう。

```java:StatementからPreparedStatementへの変更
import java.sql.PreparedStatement; // import java.sql.Statement; から変更
// (中略)
PreparedStatement stmt = null; // Statement stmt = null; から変更
// (中略)
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT name, secret FROM users WHERE ispublic = 'true' AND name='" + name
        + "' AND password='" + password + "'");
```

### 2. Connection#createStatement から Connection#prepareStatement への変更

先ほどの変更により、コンパイルエラーとなる箇所を修正します。
[Connection#prepareStatement](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/Connection.html#prepareStatement-java.lang.String-)の引数にはとりあえず空文字を入れ、コンパイルエラーが解消されることを確認します。

```java:StatementからPreparedStatementへの変更
import java.sql.PreparedStatement;
// (中略)
PreparedStatement stmt = null;
// (中略)
stmt = conn.prepareStatement(""); // stmt = conn.createStatement(); から変更
rs = stmt.executeQuery("SELECT name, secret FROM users WHERE ispublic = 'true' AND name='" + name
        + "' AND password='" + password + "'");
```

### 3. Connection#prepareStatement の引数にSQLを渡す

とりあえずコンパイルエラーは解消しました。

しかし、[JavaDoc](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/Connection.html#prepareStatement-java.lang.String-)に書いてある通り、 `Connection#prepareStatement` の引数にはSQLを渡す必要があります。このため、[Statement#executeQuery](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/Statement.html#executeQuery-java.lang.String-) の引数に渡しているSQLを移動させます。

```java:Connection#prepareStatementの引数にSQLを渡す
import java.sql.PreparedStatement;
// (中略)
PreparedStatement stmt = null;
// (中略)
stmt = conn.prepareStatement("SELECT name, secret FROM users WHERE ispublic = 'true' AND name='" + name
        + "' AND password='" + password + "'");
rs = stmt.executeQuery();
```

ここまでは、脆弱性を修正するまでの準備段階です。

### 4. パラメータ・プレースホルダーを設定する

ここからが、実際に脆弱性を修正する部分になります。

パラメータ・プレースホルダーを設定することで、機械的な処理でSQL文が組み立てられます。これにより、SQLインジェクションの脆弱性を解消できます。

まず、パラメータ部分を`?`に置換します。

```java:パラメータ・プレースホルダーを設定する
import java.sql.PreparedStatement;
// (中略)
PreparedStatement stmt = null;
// (中略)
stmt = conn.prepareStatement("SELECT name, secret FROM users WHERE ispublic = 'true' AND name=? AND password=?");
rs = stmt.executeQuery();
```

### 5. 設定したプレースホルダーにパラメータを割り当てる

[PreparedStatement#setString](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/PreparedStatement.html#setString-int-java.lang.String-)を呼び出して、さきほど設定したプレースホルダーにパラメータを割り当てます。

パラメータの数値は1から始まります。
1番目のプレースホルダーに変数`name`の値を、2番目のプレースホルダーに変数`password`の値を割り当てます。

```java:設定したプレースホルダーにパラメータを割り当てる
import java.sql.PreparedStatement;
// (中略)
PreparedStatement stmt = null;
// (中略)
stmt = conn.prepareStatement("SELECT name, secret FROM users WHERE ispublic = 'true' AND name=? AND password=?");
stmt.setString(1, name);
stmt.setString(2, password);
rs = stmt.executeQuery();
```

ここまできたら、再度、動作を確認してみましょう。
修正方法が正しければ、ほかのユーザーの情報が表示されることはなくなったはずです。

## EasyBuggy Bootの場合

SQLInjectionServletクラスの実装は、SQLInjectionControllerクラスに移植されています。

Spring Frameworkを利用しているため、[PreparedStatement](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/PreparedStatement.html)ではなく[JdbcTemplate](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html)を利用してSQLを発行しています。こちらのほうが、簡単に修正することができます。

### 1. パラメータ・プレースホルダーを設定する

EasyBuggyと同じように、パラメータ・プレースホルダーを設定したSQL文字列に置き換えましょう。

### 2. 設定したプレースホルダーにパラメータを割り当てる

[JdbcTemplate#queryでは、第3引数以降にパラメータを指定できます。](
https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#query-java.lang.String-org.springframework.jdbc.core.RowMapper-java.lang.Object...-)
メソッドを呼び出す際の引数として、パラメータを順番に渡しましょう。

おつかれさまでした。