早め早めの脆弱性対策! 開発チームでできるアプリとサーバのセキュリティ診断と要件定義の作り方
Webセキュリティ対策はなにかと面倒ですが、昨今はフレームワークが脆弱性に対応するなど、プログラミングは効率的になっています。その上でサービス全体の安全のため、開発チームがすぐ実施できるWebセキュリティ診断と要件定義について解説します。
こんにちは、松本(@ym405nm)です。
みなさんは業務やコミュニティ、趣味などでWebサイト作ってますか? SEO対策、ユーザビリティ、レスポンジブル、オートスケールなどなど、Webサイトを1つ作るだけでもさまざまな技術や考え方が必要であり、非常に奥深いものであるということは、このエンジニアHubの記事の多さが物語っているのではないでしょうか。
その中でもWebサイト開発者・運用者を悩ませるのは、Webセキュリティです。この記事では、開発フェーズから試すことができるセキュリティ診断の手法と、要件定義フェーズでやっておくべきセキュリティ対策について解説します。
- Webセキュリティに診断と要件定義がなぜ必要か
- 事例からみる脆弱性: SQLインジェクションとShellShock
- OWASP ZAPによるWebアプリケーションの脆弱性診断
- Vulsによるサーバの脆弱性検知
- セキュリティ要件定義と開発段階からのセキュリティ対策
- 終わりに
Webセキュリティに診断と要件定義がなぜ必要か
Webアプリケーションでは、動的な機能を作り込む際に脆弱性が作り込まれる可能性もあります。また昨今では、VPSやクラウドサービスなどを使用するケースもあり、ミドルウェアから自分でインストールする機会が増えました。インストールするのはよいのですが、インストールしたっきりになり、アップデートされてないまま放置され、既知の脆弱性も対策できていないまま運用されているサイトも見られます。
一般的に、Webサイトのセキュリティがどうなっているかを確認するには、セキュリティ診断(脆弱性診断)というサービスを利用します。これは診断員が攻撃者目線で疑似攻撃を行い、Webサイトに脆弱性がないかどうかを確認するサービスです。誤解を恐れずに言うと、悪いハッカーが脆弱性を見つける前に、良いハッカーが脆弱性を見つけて報告するイメージです。
通常はセキュリティ診断を提供している会社に依頼するのですが、皆さんも依頼された経験はあるでしょうか。正直なところ、見積もりを見て「高っ!」と思われた方も多いのではないでしょうか。
また、セキュリティ診断は、診断してもらって終わりではありません。帰るまでが遠足であるのと同様に、開発チームにとっては脆弱性を修正するまでがセキュリティ診断だとも考えられます。私にもセキュリティを診断した経験はいくつかあるのですが、中には大量の脆弱性が見つかったり、根本的な修正を必要とする脆弱性があったりします。その場合も、セキュリティ診断を依頼されている以上、心を鬼にして報告します。
そのような脆弱性を修正するには、追加の開発が必要になるなど、予期しないコストの増大や、スケジュールの遅れが発生します。そこでお勧めしているのは、セキュリティ対策をリリース直前にやるだけではなく、プロセスの前に戻って、要件定義のフェーズや、開発のフェーズで行うことです。
注意しておくべき点として、事前に要件定義や自己診断を行っていても、従来と同じように、専門家によるセキュリティ対策は必要です。第三者の目線で脆弱性がないかを確認し、網羅性をもった調査を実施するためです。
ただし、ここで解説するようにセキュリティの要件定義を作っておき、早い段階から自身でセキュリティ診断を行うことにより、前述したような診断結果から開発コストが増大したりスケジュールが遅れたりする可能性は大幅に下がり、健全的なプロジェクト運営が可能になります。
事例からみる脆弱性: SQLインジェクションとShellShock
この記事では、実際の脆弱性に対してセキュリティ診断を行います。ここで取り上げるいくつかの脆弱性について、事例をもとに紹介しておきます。
Webアプリケーションに埋め込まれる脆弱性の中でも、個人情報の漏えいなどを引き起こしやすいのが、SQLインジェクションです。
例えば、2019年1月にはある放送局で個人情報が約6万件漏えいしたと発表されました。この発表によると、2018年10月ごろからSQLインジェクションの攻撃が行われ、データベース内の情報を不正に取得されたとのことです。
不正アクセスによるお客様情報流出に関するお詫びとご報告| ニュースリリース
SQLインジェクションの対策が争点となった裁判事例
2014年には、SQLインジェクションによる情報漏えいで、興味深い判例がありました。
これはX社がY社に対して約900万円のWebサイトの発注したのですが、このサイトにSQLインジェクションの脆弱性があり、個人情報が漏えいしました。この件について、X社がY社に対して約1億1000万円の支払いを求めました。
私はこの話を聞いて、Y社に責任はあるものの、X社とY社は事前に契約をしており、損害賠償の制限についても何らかの契約を取り交わしているはずで、発注額の900万程度になるものかなと、なんとなく思っていました。しかしながら、判決は概ね原告の主張が認められ、発注額を大幅に超える2,262万円の損害賠償金支払いが命じられました。
ここで注目すべき点はいろいろあると思いますが、2点ご紹介します。
まずは、SQLインジェクションの対策を行う義務があったかどうかについて。裁判所は、独立行政法人情報処理推進機構(IPA)が当時公開していた文書を例に出しており、この件を当時の技術水準とし、その対策を行うことは黙示的に合意されていたとしました。つまり、裁判では、SQLインジェクション対策をするのは技術者として当然であると考えられるのです。
また、Y社の業務内容や前述の技術水準を鑑みると、本件はY社の重過失と認められました。このため、契約書に責任限定条項があったとしても、重過失により適用されないとされ、高額な損害賠償が認められました。
皆さんもWebサービスの開発を受注することがあるかもしれませんが、このように必要なセキュリティ対策を行っていないと、同様に高額の損害賠償を支払うことになるかもしれません。
私は法律の専門家ではありませんので、法律や本判決への解釈は個人的なものであり、正確ではない可能性があります。詳しくは、弁護士の伊藤雅浩さんによるブログ記事を参照してください。
クレジットカード情報の漏えい事故の責任 東京地判平26.1.23判時2221-71 - IT・システム判例メモ
サーバにおけるShellShock脆弱性
脆弱性は、アプリケーションだけではなくサーバに存在することもあります。
ShellShockという脆弱性をご存じでしょうか。これは2014年9月に、Unix/Linux系のデストリビューションでよく使用されるBashに発見された脆弱性です。
この脆弱性では任意のコードが実行され、かつ攻撃が容易であり、リモートから攻撃できることから、深刻な脆弱性であると考えられ「ShellShock」と名付けられました。
この記事でも、このShellShockをもとにサーバの脆弱性診断を行います。
Struts2の修正が間に合わなかった攻撃事例
もうひとつ、サーバにおける脆弱例の事例を紹介しておきます。
Struts2は、Javaで開発されているオープンソースのWebアプリケーション用のフレームワークです。2017年3月に、深刻な脆弱性が発見されました。
この脆弱性では、攻撃者がリモートからHTTPリクエストを送るだけで、サーバで任意コードが実行される可能性がありました。Struts2は国内でも多くのWebサイトが使用しており、大規模サイトでも運用されています。
この脆弱性を悪用されて、国内のある決済代行サービスが攻撃をうけ、クレジットカード情報が漏えいしました。被害を受けたサービスの運営会社が発表した報告書によると、脆弱性を長期間放置していたわけではなく、修正が間に合わなかったとのことです。
報告書には、脆弱性が公開された約17時間後に攻撃コードが公開され、その約13時間後に当該システムに攻撃が到達したとのことです。サービスの特性上、1~2日間で修正を適用することは現実的に難しく、攻撃を受けしまったことが分かりました。
OWASP ZAPによるWebアプリケーションの脆弱性診断
ここからは、実際に脆弱性を診断してみましょう。
OWASP ZAP(Zed Attack Proxy)は、OWASP(Open Web Application Security Project )が管理している、オープンソースのプロキシ型の診断ツールです。
Webアプリケーションの脆弱性を確認する際には、プロキシ型の診断ツールがよく使用されます。プロキシ型というのは、下図のように動作します。

通常、Webサイトにアクセスする際は、上段のようにブラウザからサーバに対してリクエストを送り、サーバ側の処理を終わったあとにレスポンスを返します。
ここに、プロキシサーバのように診断ツールを挟みます(下段)。プロキシとしても動作しますので、リクエストとレスポンスを確認できますが、診断ツールではリクエストを変更でき、自由にリクエストを送る、またはリクエストパラメータをもとにスキャンできます。
それでは、OWASP ZAPをインストールしてみましょう。
各OS環境のインストーラーが上記のページからダウンロードできます。また、実行にはJava環境(JRE)が必要です。
インストーラーを実行後、OWASP ZAPを起動して次の画面が表示されればOKです。

類似の商用製品としては、PortSwigger社が提供しているBurp Suiteなどがあります。Burp Suiteは、セキュリティ診断の現場でも使用される機会が多く、日本ではユーザグループがあり、イベントや議論が活発的に行われています。
サンプルとなるアプリケーションを用意する
続いて、脆弱性を診断するWebアプリケーションを用意しましょう。問題のないアプリケーションにSQLインジェクションを意図的に仕込んで、脆弱性の検証と対策を体験することにします。
フレームワークなどを使い始めるときに、チュートリアルとして簡単なアプリケーションを作ってみることがあります。今回は、PHPフレームワークのLaravel用のチュートリアルとして公開されている、次のTODOアプリを使用します1。
このアプリを使ったチュートリアルは下記にあります。
注意! 誤解がないよう重ねて説明すると、このアプリケーションに脆弱性があるわけではありません。後で解説しますが、このアプリではSQLインジェクション対策がされたORマッパーを適切に使用しています。
本記事では、これを意図して脆弱になるよう書き換えます。決して本記事のようなコードを書いてはいけません。
それでは環境を作ります。サクッとDockerで立ち上げましょう。LaravelのDocker環境は、こちらのものを使います。
適当なディレクトリを作ります。
$ mkdir myapp $ cd myapp
Docker環境とTODOアプリの両方をクローンします。
$ git clone https://github.com/ucan-lab/docker-laravel.git $ git clone https://github.com/MasahiroHarada/laravel-tutorial.git
環境のソース部分docker-laravel/.env
を変更します。
COMPOSE_PROJECT_NAME=laravel-tutorial PROJECT_PATH=../laravel-tutorial
Laravelのenvファイルを作成します。
$ cd ./laravel-tutorial
$ cp .env.example .env
デバッグログがWebブラウザに表示されることにより診断ツールが誤検知するのを避けるため、envファイル内のAPP_DEBUG
をfalse
にします。
docker-compose
で立ち上げます。
$ cd ./docker-laravel $ docker-compose up -d
Laravel内のComposerの依存を追加して、インストールします。
$ docker-compose exec app composer require predis/predis
これでTODOアプリが立ち上がりましたが、まだデータが何もない状態なので、次のユーザアカウント2つを登録して、アプリ内のタスクを管理するフォルダを作成します。
ユーザ名 | メールアドレス | パスワード | 作成するフォルダ |
---|---|---|---|
UserA | userA@example.com | test1234 | a1 a2 |
UserB | userB@example.com | test1234 | b1 |
Webブラウザからアプリのフォームで登録することもできますが、今回はLaravelのシーダー2で設定します。データベースをマイグレーションして、シーダーファイルを作成します。
$ docker-compose exec app php artisan migrate
database/seeds
にあるシーダーファイルを、次のように書き換えます。
UserTableSeeder.php
public function run() { DB::table('users')->insert([ 'name' => 'UserA', 'email' => 'userA@example.com', 'password' => bcrypt('test1234'), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); DB::table('users')->insert([ 'name' => 'UserB', 'email' => 'userB@example.com', 'password' => bcrypt('test1234'), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); }
FolderTableSeeder.php
public function run() { DB::table('folders')->insert([ 'title' => "a1", 'user_id' => 1, # UserA 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); DB::table('folders')->insert([ 'title' => "a2", 'user_id' => 1, # UserA 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); DB::table('folders')->insert([ 'title' => "b1", 'user_id' => 2, # userB 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); }
DatabaseSeeder.php
public function run() { $this->call(UsersTableSeeder::class); $this->call(FoldersTableSeeder::class); }
これをデータベースに流し込みます。
$ docker-compose exec app php artisan db:seed
この状態で、Webブラウザからhttp://127.0.0.1:10080/
にアクセスすると、次のログイン画面が表示されます。

userA@example.com
とtest1234
でログインしてみましょう。

図のように画面左に「a1」「a2」のフォルダが登録されており、先ほどのデータが反映されていることが分かります。「b1」に関しては別ユーザ(UserB)が登録していることになっているので、ここでは表示されません。
Webブラウザでプロキシを設定する
次に、Webブラウザでプロキシを設定します。これは前述のように、ブラウザからアプリへのリクエストをZAPに送るため必要です。ここでは、Firefoxを使って解説します。
Firefoxの設定画面で、一般(デフォルト)の一番下に「ネットワーク設定」があります。左の「接続設定...」ボタンを押してください。

プロキシの設定画面が表示されるので「手動でプロキシーを設定する」をチェックし、HTTPプロキシーに127.0.0.1
、ポートに8080
を入力してください。すぐ下の「すべてのプロトコルでこのプロキシーを使用する」にもチェックを入れて「OK」を押します。

8080番は、OWASP ZAPがデフォルトで使用するポートです。これは、ZAPの設定画面から「ローカル・プロキシ」メニューで変更できます。
実際に診断を行う際には、プロキシを頻繁に切り替えることがあります。Firefoxには、数回のクリックでプロキシ設定を切り替えられるアドオンがあります。
FoxyProxy Standard ‐ Firefox向け拡張機能を入手
このアドオンでは、URLのルールによりプロキシ経由かどうかを自動的に振り分けることもできます。
プロキシが設定できたところで、あらためてWebブラウザから対象アプリ(http://127.0.0.1:10080/
)にアクセスしてみましょう。おそらく下図のような警告が表示されます。

これは、HTTPS接続に必要なサーバ証明書が自己証明書(オレオレ証明書と呼ぶ人もいます)だという警告です。ZAPのHUD(Heads Up Display)機能3がデフォルトで有効になっていて、これがHTTPSで通信するためです。
警告画面の「詳細情報...」ボタンをクリックして「危険性を承知で続行」を選択すると接続できますが、 今回はそもそもアプリがHTTPSではないので、この機能を無効にすれば警告を回避できます。
ZAPのオプションから「HUD」を選び、一番上の「Enable When using the ZAP Desktop」のチェックを外してください。

再度アプリにアクセスします。このときhttps://
ではなくhttp://
に戻ってることを確認してください。
SQLインジェクションの実装と実行
それでは、現状では安全なTODOアプリに、脆弱性をわざわざ仕込んでいきます。
app/HTTP/Controllers/TaskController.php
内のindex
アクションを、以下のように書き換えます。
public function index(Folder $folder, Request $request) { // ユーザーのフォルダを取得する $user_id = Auth::user()->id; $query_title = $request->input('q'); if($query_title){ $folders = DB::select('select * from folders where user_id = '. $user_id .' and title = "' . $query_title . '"; '); }else{ $folders = DB::select('select * from folders where user_id = '. $user_id .' ; '); } // 選ばれたフォルダに紐づくタスクを取得する $tasks = $folder->tasks()->get(); return view('tasks/index', [ 'folders' => $folders, 'current_folder_id' => $folder->id, 'tasks' => $tasks, ]); }
また、同ファイルの上部に以下を追記します。
use Illuminate\Support\Facades\DB;
書き方を変えましたが、内容は同じで、ログインしているユーザが作ったフォルダ一覧を取得しています。ここに機能を追加して、GETパラメータにq
があった場合、その値と完全一致したものだけ取得するようにしました。簡易フィルタリング機能のイメージです。
本来なら部分一致にしたりユーザ向けのフォームを作ったりすべきですが、脆弱性は無事に仕込めたので省略します。動作を確認します。
http://127.0.0.1:10080/folders/1/tasks?q=a1
上記のURLにアクセスしてください。「a1」フォルダだけがフィルタリングされているはずです。
ここで、パラメータのa1
を以下のように変更します。
http://127.0.0.1:10080/folders/1/tasks?q=a1" or 1=1; -- ;
すると、下図のようにフォルダが全て表示されます。

ここで注目したいのは、UserBが登録したはずのフォルダ「b1」も表示されていることです。
先ほど改変したソースコードで、以下の部分に注目してください。
$folders = DB::select('select * from folders where user_id = '. $user_id .' and title = "' . $query_title . '"; ');
$query_title
にはa1" or 1=1; -- ;
が入ると、結果としてSQLは以下のようになります。
select * from folders where user_id = 1 and title = "a1" or 1=1; -- ;";
a1
のあとに入力された"
がSQLの文字列を囲んでいるものと認識されてしまい、以降の文字列がSQLクエリとして評価されます。ここですかさずor 1=1;
と書いています。最後に以降のSQL文は消したいのでMySQLでコメントアウトを意味する--
で終えます。
こうすることにより、folders
テーブルの全てを表示するSQL文が発行されます。これがSQLインジェクションの仕組みです。
環境にもよりますが、場合によってはより危険なSQL文が攻撃者に発行され、データベース内の改ざんや消去などが行われる可能性があります。
SQLインジェクションの検証
それでは先ほど紹介したOWASP ZAPを使用して、脆弱性の検証を試してみましょう。
ZAPを起動します。SQLインジェクションのアドオンも入れておきましょう。
図のようにアドオン管理画面で、Advanced SQLInjection Scanner
がインストールされていることを確認してくささい。

この画面は、下図のボタンを押すと表示されます。

ZAPのプロキシが通ってる状態で、http://127.0.0.1:10080/folders/1/tasks?q=a1
にアクセスします。下部の履歴で、次のように表示されます。

この状態でアラートタブをみると、既にいくつか検知されています。これはPassive Scan(受動的スキャン)と呼ばれ、アクセスするだけで分かる情報が出ています。

次に、Active Scan(動的スキャン)を行います。
ZAPではデフォルトでは「プロテクトモード」の状態になっており、動的スキャンできないようになっています。右上のリストからアプリを右クリックして、コンテキストに追加します。これで攻撃対象になります。

追加するときには、URLを正規表現で指定できます。

右上のサイトリストが「的」のようなアイコンに変わってることが分かります。

再び右クリックして[攻撃]→[動的スキャン]を選択します。

ダイアログで「動的スキャン」の設定画面が表示されます。今回はデフォルトのまま[OK]を押して、しばらく待ちます。

アラートを見てみると「SQLインジェクション」が検知されていることが分かります。

今回は事前に脆弱性を仕込んでから試したので、うまく検知されていることが分かります。
ただし、本来は脆弱性があるかどうか分からない状況でスキャンしますから、アラート情報をヒントに手動で検証することになります。自動的なスキャンですので、どうしても「脆弱性があるのに見逃してしまった」あるいは「脆弱性がないのにアラートが出た」といった誤検知もあります。
SQLインジェクションの対策 ─ サンプルのアプリが安全な理由
この結果を見て「Laravelやべー」と思った方がいるかもしれませんが、もちろんそれは誤解です。
今回のアプリケーションで、改変する前のソースコードがどうだったかを確認してみましょう。
public function index(Folder $folder) { // ユーザーのフォルダを取得する $folders = Auth::user()->folders()->get(); //省略 }
ここでは、まずユーザ情報を取得して、ユーザモデルのfolders()
関数を呼び出しています。
モデル側ではこのように受け取っています。
public function folders()
{
return $this->hasMany('App\Folder');
}
このように取得することで、Laravelに実装されているORマッパーを使用できます。
LaravelのORマッパーはEloquentと呼ばれますが、このように主要なフレームワークで使用されるORマッパーでは、SQLインジェクション対策がされています。
一方で、私が改変したソースコードのように、SQL文を文字列連結で作ると、脆弱性が作り込まれることがあります。今回のパラメータでは"
をエスケープすることで攻撃を防ぐことはできますが、エスケープでは対策漏れの可能性もあります。
データの取得などを独自実装することは避け、必ずフレームワークが推奨する方法を使用しましょう。
Vulsによるサーバの脆弱性検知
続きをお読みいただけます(無料)。

- すべての過去記事を読める
- 過去のウェビナー動画を
視聴できる - 企業やエージェントから
スカウトが届く