SQLインジェクションをやってみたいんだ！

# 概要
やや物騒なタイトルから始まりましたが、SQLインジェクションを体験できる記事になります！
SQLインジェクションを解説した記事は多くあリますが、リポジトリを公開し簡単にSQLインジェクションを体験できる記事がなかったため、執筆しました。リポジトリを公開しているので、実際に体験できます。また、本記事では、実際に体験しどのような危険性があるのか、またその対策について考えてみました。
簡単なユーザー登録画面とログイン画面を作成しています。
以下がリポジトリです。

https://github.com/clcl777/SQL_injection

# SQLインジェクションとは
そもそも、インジェクション（Injection）とは、入力フォームなどの文字列の入力を受け付けるプログラムに対し、不正な文字列を入力することでデータの改ざんや詐取を行うサイバー攻撃のひとつです[^1]。
その一つであるSQLインジェクションとは、アプリケーションの脆弱性を利用し、アプリケーションが想定しないSQL文（データベースへの命令文）を実行させることで、データベースを不正に操作する攻撃方法のことです[^2]。 つまり、入力フォームなどにSQL文を含めた文字列を入力して、不正にSQL文を実行することです。実際に、webアプリケーションへの攻撃の約66%がSQLインジェクションが原因であるという調査があります[^3]。
他にもインジェクションには、JavaScriptインジェクション、プロンプトインジェクションなどがあります。

# 起動方法
以下のリポジトリをcloneします。

https://github.com/clcl777/SQL_injection

その後、プロジェクト配下で、以下を実行しコンテナをビルド、起動します。
```shell
make build
make up
```

:::note info
もし、Rancher Desktopを使用していてPermission deniedのエラーが出る場合は以下をご確認ください。
https://qiita.com/tra_/items/73d3ae07a645fd5b0f36
:::

以下でアクセスできます。
http://localhost:8000/

# ソースコード
今回、サーバーはPythonのFlask、データベースはPostgreSQLを採用しています。

本来、パスワードはハッシュ化などを施して保存されるべきですが、簡単のため実装していません。また、ログインセッションの保持なども行いません。

以下でテーブル定義をします。
```init.sql
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL
);
```

以下はユーザー登録画面です。フォームから名前とパスワードを受け取って、SQL文を用いてDBに保存しています。
```Python
@app.route("/signup", methods=["GET", "POST"])
def signup():
    if request.method == "POST":
        name, password = request.form["name"], request.form["password"]
        with get_connection() as conn, conn.cursor() as cur:
            cur.execute(f"INSERT INTO users (name, password) VALUES ('{name}', '{password}')")
            conn.commit()
        return redirect("/")
    else:
        return render_template("signup.html")
```
そして、以下がログイン画面です。
フォームから名前とパスワードを受け取り、一致するユーザーがいた場合は、マイページへ遷移し、それ以外は、「Invalid username or password」と表示します。
```Python
@app.route("/", methods=["GET", "POST"])
def signin():
    if request.method == "POST":
        name, password = request.form["name"], request.form["password"]
        with get_connection() as conn, conn.cursor() as cur:
            cur.execute(f"SELECT * FROM users WHERE name = '{name}' AND password = '{password}'")
            user = cur.fetchone()
            if user:
                return redirect("/mypage")
            else:
                flash("Invalid username or password", "flash-error")
        return redirect("/")
    else:
        return render_template("signin.html")
```
以下で http://localhost:8000/admin にアクセスすると、DBの中身が見れるようにしてあります。
```Python
@app.route("/admin")
def admin():
    with get_connection() as conn, conn.cursor() as cur:
        cur.execute("SELECT * FROM users")
        users = cur.fetchall()
    return render_template("admin.html", users=users)
```

# 登録からログインまで
コンテナ起動後に http://localhost:8000/signup にアクセスして、ユーザ名を`user1`、パスワードを`pass1`で登録します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954807/04d92cf4-9c83-ff89-6a38-c617a4395d1d.png" width="500">

http://localhost:8000/admin にアクセスすると、ユーザが登録されていることがわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954807/18d0109c-e611-e1d5-341b-8e2901b90aa0.png" width="250">

次に、http://localhost:8000/ にアクセスして、先ほど登録したユーザ名とパスワードでログインします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954807/83983b72-4c39-255e-aca0-839240348853.png" width="500">

ユーザ名とパスワードが一致すると、ログインが成功して、マイページに遷移されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954807/f64e1f14-8f44-525e-db02-066b6af6b2de.png" width="200">

# 脆弱性
先ほどのソースコードでは、実行するSQL文に対して、単なる文字列結合が行われており、ユーザーが任意のSQLを実行することができてしまいます。それを利用して、不正にログインをしてみます。例えば、パスワードを知らない悪意のある他者が、以下のパスワードでログインを試みます。ユーザ名を`user1`、パスワードを`' OR 1=1; -- `にして入力します。

すると、
```Python
cur.execute(f"SELECT * FROM users WHERE name = '{name}' AND password = '{password}'")
```
の部分で文字列結合が行われ以下のクエリが実行されます。
```SQL
SELECT * FROM users WHERE name = 'user1' AND password = '' OR 1=1; -- '"
```
このとき、1=1は必ずtrueになるため、全件取得されてしまいます。一致するレコードが存在するかどうかでログイン判定を行なっているため、パスワードが不明でも不正にログインできてしまいます。最後の`--`によりそれ以降をコメントアウトしています。これがおそらく一番有名なSQLインジェクションの例です。検索エンジンのサジェスト候補にも出てきます。

![Screenshot 2024-09-16 at 21.15.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954807/31f4ef86-8f03-7802-f5c7-5eab836fb10b.png)


他にも、ユーザー登録画面でユーザ名を`user2`、パスワードを以下のように入力します。
```
pass2'); INSERT INTO users (name, password) VALUES ('Fake User', 'Fake Password'); -- 
```
すると、文字列結合が行われ以下のクエリが実行されます。
```SQL
INSERT INTO users (name, password) VALUES ('user2', 'pass2'); INSERT INTO users (name, password) VALUES ('Fake User', 'Fake Password'); -- ')
```
そして、user2、pass2という正規のユーザー以外に、Fake User、Fake Passwordという不正なユーザーが作成されてしまいます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954807/87cc7390-846b-0410-188f-eae5a973b771.png" width="250">

# 対策
SQLインジェクションの対策方法として、2つ紹介します。
## 1. プレースホルダーを使用する
以下のように書き換えて文字列の直接代入を回避します。
```Python
    - cur.execute(f"SELECT * FROM users WHERE name = '{name}' AND password = '{password}'")
    + cur.execute("SELECT * FROM users WHERE name = %s AND password = %s", (name, password))
```
```Python
    - cur.execute(f"INSERT INTO users (name, password) VALUES ('{name}', '{password}')")
    + cur.execute("INSERT INTO users (name, password) VALUES (%s, %s)", (name, password))
```
プレースホルダーにより、ユーザーからの入力データは自動的にエスケープされて、SQLコマンドの一部として扱われなくなります。よって、SQLインジェクションのリスクが防げます。

## 2. ORMを使用する
ORM（Object-Relational Mapping）は、データベースとアプリケーションの間でデータのやり取りを行う際にSQL文を自動生成し、開発者がSQL文を直接書く必要がありません。そのため、SQLインジェクションのリスクを軽減できます。例えば、FlaskでSQLAlchemyを使用する場合、次のようにしてSQLをエスケープし、安全なデータベース操作を行うことができます。
以下でテーブルを定義します。
```Python
class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    password = db.Column(db.String(255), nullable=False)
```
以下が、ユーザ登録画面と、ログイン画面です。
```Python
@app.route("/", methods=["GET", "POST"])
def signin():
    if request.method == "POST":
        name, password = request.form["name"], request.form["password"]
        user = User.query.filter_by(name=name, password=password).first()
        if user:
            return redirect("/mypage")
        else:
            flash("Invalid username or password", "flash-error")
        return redirect("/")
    else:
        return render_template("signin.html")


@app.route("/signup", methods=["GET", "POST"])
def signup():
    if request.method == "POST":
        name, password = request.form["name"], request.form["password"]
        user = User(name=name, password=password)
        db.session.add(user)
        db.session.commit()
        return redirect("/")
    else:
        return render_template("signup.html")
```
ORMを使用することでSQLを直接記述することがないので、SQLインジェクションを防ぐことができます。
リポジトリのORMブランチにORMを使用したバージョンのコードがあります。

https://github.com/clcl777/SQL_injection/tree/ORM

# まとめ
この記事では、SQLインジェクションの脆弱性を体験し、SQL文の文字列結合による危険性と、不正な操作がどのように行われるかについて書きました。
対策として、プレースホルダーの使用やORMの導入が有効であり、これによりセキュリティを強化できます。

[^1]: [インジェクションとは](https://www.sompocybersecurity.com/column/glossary/injection) - 損保ジャパン 日本サイバーセキュリティ・アナライジス株式会社

[^2]: [SQLインジェクションとは](https://www.sompocybersecurity.com/column/glossary/sql-injection) - 損保ジャパン 日本サイバーセキュリティ・アナライジス株式会社

[^3]: [国内からの攻撃増加 手法は SQLインジェクション最多](https://scan.netsecurity.ne.jp/article/2024/08/01/51398.html) - ScanNetSecurity