広告

【Laravel】敢えて試すSQLインジェクション【仕組みを知るには実装】

Laravel

当サイトではアフィリエイト広告を利用しています。

広告

はじめに

最近話題になりましたSQLインジェクション
いわゆる脆弱性ですが、とあるWebシステムで見つかったそうです。(すっとぼけ)

ワクチン予約システムで話題の「SQLインジェクション」って何? 試すと法律違反? 専門家に聞く
「SQLインジェクションができる」と話題になった大規模接種センターの新型コロナワクチン接種予約システム。Twitterには実際に試すと犯罪になるという声もあったが、そもそもSQLインジェクションとは何で、実行すると本当に犯罪になるのか。専門…

「SQLインジェクションなんて初歩的なミスを・・」とか、「名前は聞いたことあるけど・・」とか、「最近のフレームワークを使っていれば安心でしょ?」など、色々な意見があるかと思います。

今回は、敢えてSQLインジェクションの脆弱性を含んだシステムを作成してみたいと思います。
また、人気のあるLaravelというフレームワークで試します。

Laravelを使用したことがない人でも構築できるように、イチから解説しています。
対策や仕組みを知るには、実際に試してみるのが手っ取り早いですよね!?

本記事ではLaravelを使用して実際に試してみますが、SQLインジェクションはどの言語・フレームワークでも概念は同じです。
しっかりと仕組みを押さえておきましょう。

他にも私のブログで、Laravelについて解説している記事がありますのでご覧ください。

お断り

試す場合は、プライベートな環境(ローカル等)で行いましょう。
また、脆弱性のあるコードを本記事では扱いますが、使用しないように気をつけてください。

OWASP ZAPでの脆弱性診断

脆弱性診断をおこなう無料のツールがあります。
詳しくは以下の記事をご覧ください。

【OWASP ZAP】インストールと基本的な使い方【Web脆弱性診断】
OWASP ZAPを使用して、Webの脆弱性診断を行ってみます。オープンソースで無料で使うことができます。
【OWASP ZAP】CSRF対策ページに脆弱性診断
CSRFの対策を行っているサイトに対して、OWASP ZAPを利用して脆弱性診断を行います。そのための設定や、実際に攻撃を行い動作確認を行いました。

環境

  • Windows 11 or macOS Monterey (M1)
  • Laravel Framework 9 or 10
  • PHP 8

2022/04/19 最新バージョンで確認しました。
2023/03/11 Laravel10で確認しました。

広告

【紹介】個人開発

私の個人開発ですがQuiphaというサービスを開発しました。(Laravel, Vue3など)
良かったら、会員登録して動作を試してみて下さい。

また、Laravel 9 実践入門という書籍を出版しました。
Kindle Unlimitedを契約している方であれば、読み放題で無料でご覧いただくことができます

是非多くの方に読んでいただき、Laravelの開発に少しでもお役に立てたら幸いです。

フレームワークについて

Webシステムなどはじめ、開発においては通常はフレームワークを使用します。

フレームワークとはその名の通り土台であり、予め機能が用意されていたり、ルールが決まっているなど、不具合を埋め込みにくくするメリットもあります。

その一環として、脆弱性に対する対応も考慮されているケースもあります。
今回紹介するLaravelも、普通に実装すれば、SQLインジェクションを埋め込む可能性は低いと思います。

逆に「フレームワークを使ったら脆弱性のあるコードは入り込まないよね?」というのは間違いですので、気をつけましょう。

フレームワークを使用する場合、きちんとフレームワークのマニュアルを読み、特性を知ることが大事です。
学習コストが必要ですが、結果的には工数削減に繋がります。

Laravelで実装(問題のないコード)

それでは早速、実装を行いましょう。
いきなり脆弱性のあるコードを埋め込むのではなく、まずは問題のない実装を行います。

プロジェクトの作成

まずはプロジェクトの構築です。
Laravel Sailを使った構築は以下の記事をご覧ください。

Windows
Laravel Sailで開発環境構築【Vite対応】
今回は、Laravel Sailを使って環境の構築を行いました。 SailはDockerですので、開発環境を簡単に用意することもできますし、カスタマイズすることも楽です。
Mac
【M1 Mac】Laravel Sailで開発環境構築【Vite対応】
Laravel Sailを使って環境の構築を行いました。 SailはDockerですので、開発環境を簡単に用意することもできますし、カスタマイズすることも楽です。

以下のコマンドで、Laravelのプロジェクトを作成します。
プロジェクト名は「sql-injection」にしました。(任意です)

$ curl -s https://laravel.build/sql-injection | bash

Sailを起動します。

$ sail up -d

Sailコマンドで、npmパッケージのインストールとビルドを行います。

$ sail npm install
$ sail npm run dev

以下のURLにアクセスし、初期画面が表示されることを確認します。

http://localhost

作成するサイト

  • 簡易的なログイン画面を作成。
  • ログインIDとパスワードを入力しログインを行う。
  • IDとパスワードが一致するレコードが存在する場合は、ログイン成功とする。

テーブルの作成

テーブルを作成していきましょう。マイグレーションファイルを作成します。
初期状態でusersというテーブルが作成されますので、今回は別途アカウントテーブルを作成することにしました。

以下のコマンドを実行し、マイグレーションファイルを生成します。

$ sail php artisan make:migration create_accounts_table

マイグレーションファイルが作成されます。(ファイル名は現在時刻)

database/migrations/2021_05_19_140741_create_accounts_table.php

マイグレーションファイルを編集します。
今回は、ログインIDとパスワードの項目を追加します。(テーブル定義は細かく掘り下げません)

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('accounts', function (Blueprint $table) {
            $table->id();
            $table->string('login_id');
            $table->string('password');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('accounts');
    }
};

マイグレーションを実行し、テーブルを作成します。

$ sail php artisan migrate

この時点でMySQLのデータベースを確認し、テーブルが作成されていることを確認してください。

ダミーデータの作成

Laravelのシーダーという機能で、ダミーデータを作成します。
以下のコマンドを実行し、シーダーを作成します。

$ sail php artisan make:seeder AccountsSeeder

シーダークラスが作成されます。

database/seeders/AccountsSeeder.php

ログインIDとパスワードのダミーデータを挿入する処理を追加します。

ログイン情報
  • ログインID : login-user
  • パスワード : password
<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class AccountsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('accounts')->insert([
            'login_id' => 'login-user',
            'password' => 'password',
        ]);
    }
}

シーダーを実行します。

$ sail php artisan db:seed --class=AccountsSeeder

アカウントテーブルにデータが挿入されたことを確認しましょう。

モデルの作成

先ほど作成したテーブルに対するモデルクラスを作成します。

コマンドを実行します。

$ sail php artisan make:model Account

クラスファイルが作成されますが、特に変更の必要はありません。

画面の作成

コマンドを実行し、コントローラーを作成します。

$ sail php artisan make:controller LoginController

web.phpを修正します。
ログイン入力画面と、結果画面のルーティングを追記します。

use App\Http\Controllers\LoginController;

Route::get('/login', [LoginController::class, 'index'])->name('login-index');
Route::post('/login', [LoginController::class, 'result'])->name('login-result');

続いて、画面テンプレートを作成します。(ログイン画面)
以下のファイルを作成します。

resources/views/login.blade.php

テンプレートファイルにログインフォームを記述します。

<html>
<body>
    <form method="POST" action="{{ route('login-result') }}">
        @csrf
        <input type="text" name="login_id" placeholder="ID"><br>
        <input type="text" name="password" placeholder="PASSWORD"><br>
        <input type="submit" value="ログイン">
    </form>
</body>
</html>

@csrfは、クロスサイトリクエストフォージェリ(CSRF)の対応です。(脆弱性の一つ)
また、actionのURLはroute経由で取得すると、web.phpの定義と連動できますので保守性が良くなるでしょう。

次に、以下のファイルを修正します。

App\Http\Controllers\LoginController.php

ログイン画面の表示と、ログイン情報の確認結果を表示する処理を実装します。
ここでは、テーブルからデータを取得するのに、LaravelのEloquentというORマッパーを使用します。

O/Rマッピングとは、オブジェクト指向プログラミング言語におけるオブジェクトとリレーショナルデータベース(RDB)の間でデータ形式の相互変換を行うこと。そのための機能やソフトウェアを「O/Rマッパー」(O/R mapper)という。

https://e-words.jp/w/O-R%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0.html

フォームで入力されたIDとパスワードが、アカウントテーブルに一致するデータが存在するかどうかチェックしています。(exists)

このコードは安全です。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\Account;

class LoginController extends Controller
{

    /**
     * ログインフォーム
     */
    public function index()
    {
        return view('login');
    }

    /**
     * ログイン結果を表示
     *
     * @param  Request  $request
     */
    public function result(Request $request)
    {
        $login_id = $request->input('login_id');
        $password = $request->input('password');

        // ログイン情報の確認
        $exists = Account::where('login_id', $login_id)
            ->where('password', $password)
            ->exists();

        if ($exists) {
            return "Login OK!";
        } else {
            return "Login NG!";
        }
    }
}

それでは画面を開いてみましょう。

http://localhost/login

ログインフォームが表示れました。

IDに「login-user」、パスワードに「password」と入力しログインボタンをクリックすると、Login OK!」と表示され、ログイン成功です。
それ以外のログイン情報の場合は、「Login NG!」と表示され、ログイン失敗となります。

補足

データベースの設定次第では、大文字・小文字の区別はされない場合がありますが、今回は本質ではありませんので触れません。

引き続き、SQLインジェクションを含んだコードを実装します。

SQLインジェクションのコードを実装

実装

さて本題です。

先程までのコードは問題ないコードでしたが、どのようにすればSQLインジェクション(脆弱性あるコード)となるのでしょうか。

SQLインジェクションの場合は、SQL文を文字列連結で動的に組み立てると問題がある可能性があります。

Laravelでも生のSQLを実行する関数は一応用意されています。

10.x データベース:クエリビルダ Laravel

Laravelのマニュアルに記載のある通り、取り扱いには注意です。

素のSQL文はそのまま文字列としてクエリへ挿入されるため、SQLインジェクションの脆弱性を含めぬように細心の注意を払う必要があります。

https://readouble.com/laravel/10.x/ja/queries.html

コントローラを以下のように修正します。

whereRawを使用し、SQL文を文字列連結して引数に渡します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\Account;

class LoginController extends Controller
{

    /**
     * ログインフォーム
     */
    public function index()
    {
        return view('login');
    }

    /**
     * ログイン結果を表示
     *
     * @param  Request  $request
     */
    public function result(Request $request)
    {
        $login_id = $request->input('login_id');
        $password = $request->input('password');

        // ログイン情報の確認
        // $exists = Account::where('login_id', $login_id)
        //     ->where('password', $password)
        //     ->exists();

        // 注意:脆弱性のあるコード(SQLインジェクション)
        $exists = DB::table('accounts')
            ->whereRaw('login_id = \'' . $login_id . '\'')
            ->whereRaw('password = \'' . $password . '\'')
            ->exists();

        if ($exists) {
            return "Login OK!";
        } else {
            return "Login NG!";
        }
    }
}

この状態でログインフォームから動作確認を行うと、問題のないコードと同じように正常に動くように見えます。

では、ログインIDはデタラメで、パスワードを以下にしてみましょう。

' or 1 = 1 or '

存在しないIDとパスワードですが、Login OK!」と表示され、ログイン成功します。

あれれ、IDとパスワードが一致しないのに、

レコードが存在していることになっているよ・・

広告

SQL文の確認

それでは、どのようなSQL文が実行されているか確認してみましょう。
クエリビルダの実行処理の前後に、SQL文を出力するコードを記述します。

        \DB::enableQueryLog();

        // 注意:脆弱性のあるコード(SQLインジェクション)
        $exists = DB::table('accounts')
            ->whereRaw('login_id = \'' . $login_id . '\'')
            ->whereRaw('password = \'' . $password . '\'')
            ->exists();

        dd(\DB::getQueryLog());

上記の状態で、リクエストを送信してみましょう。
実行されるSQL文が表示されます。

array:1 [▼
  0 => array:3 [▼
    "query" => "select exists(select * from `accounts` where login_id = 'login-user' and password = '' or 1 = 1 or '') as `exists`"
    "bindings" => []
    "time" => 6.3
  ]
]

SQL文のwhere句が、不正に組み立てられ、構文としては成立します。
また、「or 1 = 1」の条件により常に真となり、不正にレコードを取得することができます。

where login_id = 'login-user' and password = '' or 1 = 1 or ''

不正なリクエストではない場合は、当然、SQL文として問題なく実行されます。

array:1 [▼
  0 => array:3 [▼
    "query" => "select exists(select * from `accounts` where login_id = 'login-user' and password = 'password') as `exists`"
    "bindings" => []
    "time" => 3.02
  ]
]

またSQLインジェクションは、SQL文を不正に組み立て実行することができるため、シングルクオートやダブルクオートをリクエストに入れると、SQLの構文エラーでシステムエラーが発生する場合が多いです。

例えば、パスワードにシングルクオートだけ入力してログインしてみましょう。

SQL構文エラーで、システムエラーが表示されます。

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ””) as `exists`’ at line 1 (SQL: select exists(select * from `accounts` where login_id = ‘hoge’ and password = ”’) as `exists`)

SQL文をリクエストパラメータの値と文字列連結で動的に作成すると、SQLインジェクションになる場合があります。

他にも

今回の例では、ログイン情報が存在するかどうかのチェックのみですが、例えばマスタメンテナンス画面で、データの一覧を表示する画面があったとします。

検索フォームを用意し、条件によって一覧を絞り込む実装はよくあると思います。
検索条件のWhere句にSQLインジェクションが含まれる場合、全てのデータを閲覧されてしまう可能性があります。
(「or 1 = 1」の条件によりすべてのデータを取得)

シンプルに情報流出にもなり、大変危険です。

プレースホルダーの利用

どうしてもwhereRawを使用しないといけない場合は、プレースホルダーでバインド変数を活用しましょう。
この場合は、SQLインジェクションにはなりません。

        $exists = DB::table('accounts')
            //->whereRaw('login_id = \'' . $login_id . '\'')
            //->whereRaw('password = \'' . $password . '\'')
            // プレースホルダーとバインド変数を利用する
            ->whereRaw('login_id = ?', [$login_id])
            ->whereRaw('password = ?', [$password])
            ->exists();

先程同様に、パスワードに不正なリクエストを含めて実行してみます。
今度は、「Login NG!」となり正常な動作になります。

' or 1 = 1 or '

SQL文を確認してみると、バインド変数が利用されているのが分かります。(SQL文自体は動的に変更されていない)

array:1 [▼
  0 => array:3 [▼
    "query" => "select exists(select * from `accounts` where login_id = ? and password = ?) as `exists`"
    "bindings" => array:2 [▼
      0 => "login-user"
      1 => "' or 1 = 1 or '"
    ]
    "time" => 4.07
  ]
]

とはいえ、whereRawを極力使用しないほうが良いでしょう
他のフレームワークでも、SQL文を動的に作成して実行するのは避けたほうが良いと思います。

さいごに

SQLインジェクションを含んだコードを実装してみました。
最悪、データを不正に閲覧したり、削除される恐れもあります。

ちなみに公開されているサイトで、SQLインジェクションや脆弱性の確認は、不正アクセス禁止法に触れる場合もあるかもしれませんし、何よりもTwitterなどで公にせず、IPAに連絡をしましょう。

脆弱性を突く手口、IPA「見つけたらまず開発者やIPA窓口に報告して」【訂正あり】
大規模会場を使った新型コロナワクチンの接種予約システムの欠陥を巡り、情報公開の在り方で議論が起きている。IPA(情報処理推進機構)は5月18日、取材に対し「一般論ではあるが、脆弱性や手口を不特定多数に公開するのは望ましくない」とコメントした…

フレームワークを正しく利用すれば、概ね問題ないとは思いますが、脆弱性の仕組みを知ることにより、埋め込まないように気をつけましょう。

他にも私のブログで、Laravelについて解説している記事がありますのでご覧ください。

LaravelMacOSPHPWindowsプログラミング作ってみよう
広告

個人開発

千草 @chigusaweb

現役のITエンジニア。 気ままにコードを書いたり技術情報を発信しています。 Webアプリ/Windows・Macアプリ/モバイルアプリなど。 (Java, PHP, Javascript, Swift, Python, C#, 他) 個人開発:Clibor, Quipha, TXT-Crypter, 符計算特訓, チグサツール Kindle本: Laravel9 実践入門, 他

クリップボード履歴

Clibor

Windows

Cliborはシンプルで高機能なクリップボード履歴ソフトです。
また普段よく使うワードを定型文として登録し、いつでもクリップボードに保存することができます。高度なテキスト整形・FIFOモード・ホットキーに対応。

クリップボード履歴

Windows版Cliborの利便性を、そのままMacでも。
定型文登録、高度なテキスト整形、FIFOモードなど、便利なクリップボード履歴機能を利用できます。macOS最新のTahoeにも対応。

テキスト暗号化

テキストを暗号化してURLで共有・保存できる無料サービスです。
パスフレーズを知る人だけが復号できます。登録不要、データはサーバーに保存されません。
Notionでも利用できます。

学習

Quipha

Web / iOS

自分だけの問題集や問題を作成し、クイズを行い、学習に活用することができるアプリです。
例えば、学校の授業、語学学習、IT資格やその他の資格など多彩な分野での学習を支援します。
いつでも、どこでも、あなたの学習をサポート。

ツール

日常の「ちょっとした効率化」をサポートするWeb便利ツール集。
テキスト・データ処理から最新のAI連携まで、日々のちょっとした手間でググりがちなユーティリティを1つの場所に集約。

麻雀

麻雀の符計算をひたすら特訓しマスターしましょう。
初心者の方はもちろん、もっと速く計算したい方にも役立ちます。
5万対局以上の実践から問題を収録。

コメント

タイトルとURLをコピーしました