9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

概要

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

SQLインジェクションとは

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

起動方法

以下のリポジトリをcloneします。

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

make build
make up

もし、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に保存しています。

@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」と表示します。

@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の中身が見れるようにしてあります。

@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で登録します。

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

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

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

脆弱性

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

すると、

cur.execute(f"SELECT * FROM users WHERE name = '{name}' AND password = '{password}'")

の部分で文字列結合が行われ以下のクエリが実行されます。

SELECT * FROM users WHERE name = 'user1' AND password = '' OR 1=1; -- '"

このとき、1=1は必ずtrueになるため、全件取得されてしまいます。一致するレコードが存在するかどうかでログイン判定を行なっているため、パスワードが不明でも不正にログインできてしまいます。最後の--によりそれ以降をコメントアウトしています。これがおそらく一番有名なSQLインジェクションの例です。検索エンジンのサジェスト候補にも出てきます。

Screenshot 2024-09-16 at 21.15.19.png

他にも、ユーザー登録画面でユーザ名をuser2、パスワードを以下のように入力します。

pass2'); INSERT INTO users (name, password) VALUES ('Fake User', 'Fake Password'); -- 

すると、文字列結合が行われ以下のクエリが実行されます。

INSERT INTO users (name, password) VALUES ('user2', 'pass2'); INSERT INTO users (name, password) VALUES ('Fake User', 'Fake Password'); -- ')

そして、user2、pass2という正規のユーザー以外に、Fake User、Fake Passwordという不正なユーザーが作成されてしまいます。

対策

SQLインジェクションの対策方法として、2つ紹介します。

1. プレースホルダーを使用する

以下のように書き換えて文字列の直接代入を回避します。

    - 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))
    - 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をエスケープし、安全なデータベース操作を行うことができます。
以下でテーブルを定義します。

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)

以下が、ユーザ登録画面と、ログイン画面です。

@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を使用したバージョンのコードがあります。

まとめ

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

  1. インジェクションとは - 損保ジャパン 日本サイバーセキュリティ・アナライジス株式会社

  2. SQLインジェクションとは - 損保ジャパン 日本サイバーセキュリティ・アナライジス株式会社

  3. 国内からの攻撃増加 手法は SQLインジェクション最多 - ScanNetSecurity

9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?