「Djangoのセキュリティについて考える」SQLインジェクション編

この記事は以下のターゲットを対象としています。

★5 Djangoの開発経験が3年以上。
★4 Djangoの開発経験が1年以上。
★3 WEBサイト開発経験あり。これからDjangoを学習します。
★2 Python初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。

こんにちは、グローバルウェイの清家です。
グローバルウェイの開発標準とするDjangoに関するノウハウを紹介します。

前回に引き続き、「Djangoのセキュリティについて考える」をテーマにSQLインジェクションについて記事をお届けします。

目次

SQLインジェクションとは?

WEBアプリケーションでは、入力情報を基にデータベースからさまざまな情報を取得します。リレーショナルデータベースでは、SQLを利用して情報を取得しますが、SQLの組み立て方法に適切な対応がなされていない場合、攻撃者は悪意のあるクエリを注入し、不正なSQLを実行させることができます。
その結果、データベース上の情報が盗まれたり、ログインパスワード等、重要な情報が改ざんされる可能性があります。開発者はSQLの組み立て方法について、プログラミング言語やフレームワークに応じた適切な対策を行う必要があります。

SQLインジェクションを体験しよう

実際にサンプルコードを基にSQLインジェクションを起こしてみます。
以下にaccountsテーブルとデータベース操作に関するライブラリを用意します。
accountsテーブルには、id: 1、login_id:” sql.injection.test”、password:”password”のデータを投入します。

-- DDL(accountsテーブル)

create table public.accounts (
  id bigint not null
  , login_id text
  , password text
  , primary key (id)
);
# Python

SQL_INJECTION = """
SELECT
    id,
    login_id,
    password
FROM
    accounts
WHERE 
    login_id = '{}'
    AND password = '{}'
"""


def get_account_with_sql_injection(login_id: str, password):
    """
    ログイン処理(SQLインジェクション)
    @param login_id:
    @param password:
    @return:
    """
    raw_query_set = Accounts.objects.raw(SQL_INJECTION.format(login_id, password))
    for v in raw_query_set:
        return v
    return None

サンプルコードを基にしたWEBアプリケーションを実際に動かしてみます。

以下のログイン操作では、login_id、passwordが一致しないため、入力エラーとなります。正しい挙動を示しています。

では、次の入力例ではどうでしょうか。

ログインしました。
passwordが一致しないにもかかわらず、正常に画面遷移されました。
format関数によるSQLの埋め込み処理を行いますが、適切なエスケープ処理がされていないため、悪意のあるクエリが注入される恐れがあります。login_idさえわかれば誰でもなりすましによるログインが可能です。
実際には以下のようなSQLが実行されます。

-- SQL

SELECT id, login_id, password FROM accounts WHERE login_id = 'sql.injection.test' AND password = 'test' OR '1'='1';

ORMを使用する

DjangoにおけるSQLインジェクションの対策をいくつか紹介します。
一つ目はORMによる対策です。
ORMとは、Object Relational Mappingの略称を表し、オブジェクト指向プログラミングのオブジェクトとデータベースのマッピングをします。PythonプログラムによりデータベースのCRUD操作を行えます。

以下にサンプルコードを示します。
SQLを生成するときに自動的にエスケープ処理が行われるため、SQLインジェクションを防げます。

# Python

def get_account_with_orm(login_id: str, password):
    """
    ログイン処理(ORM)
    @param login_id:
    @param password:
    @return:
    """
    try:
        return Accounts.objects.get(login_id=login_id, password=password)
    except Accounts.DoesNotExist as e:
        return None

生のSQLを使用する

二つ目は生のSQLを使用する場合の対策です。
ORMによる表現が難しい複雑なSQLを使用したいケースも出てくるかと思います。

以下にサンプルコードを示します。
生のSQLを使用する場合、プレースホルダという仕組みを利用します。
SQL文の中の動的に変更したい箇所に”%s”の文字列を割り当てます。
今回の例だと”%s”の箇所にpasswordの値が代入され、’(シングルクォーテーション)のような特殊な文字列はエスケープ処理されます。

# Python

SQL = """
SELECT
    id,
    login_id,
    password
FROM
    accounts
WHERE 
    login_id = %s
    AND password = %s
"""


def get_account_with_orm(login_id: str, password):
    """
    ログイン処理(ORM)
    @param login_id:
    @param password:
    @return:
    """
    raw_query_set = Accounts.objects.raw(SQL, [login_id, password])
    for v in raw_query_set:
        return v
    return None

まとめ

SQLインジェクションはデータの改ざん、情報漏洩という点で最重要なセキュリティ課題となります。
基本的にORMを使用すれば防げますが、生のSQLを使用する機会も珍しいことではありません。特にチーム開発となると、SQLインジェクションを埋め込んでしまうミスも起こりがちです。普段からチーム内で情報共有をする、有識者によるレビューを実施するといったことを習慣づけることが重要です。

次回以降もまた、DjangoにおけるWEBアプリケーションのセキュリティリスクを取り上げ対策を説明したいと思います。

この記事が気に入ったら
いいね または フォローしてね!

  • URLをコピーしました!
  • URLをコピーしました!
目次