本章では、アプリ開発時に知っておくべき「セキュリティ」について説明します。
2014年(平成26年)1月23日に、システム開発時のセキュリティ対策の不備により発生した個人情報漏洩事件について、システム開発を発注した会社と開発を請け負った会社とが争った裁判の判決が出ました。結果は開発側の「債務不履行」が認められる判決となっています1。 いわゆる「徳丸本」で有名な徳丸浩氏が、この裁判についてわかり易く解説したブログ2を公開しています。
アプリ開発におけるセキュリティ対策の責任は、開発する人だけではなく開発を依頼する人にもあると考えます。上記の判例では、開発側に対して厳しい判決が言い渡されました。しかし、個人情報を安全に取り扱う仕様の提案を採用しなかった発注者の責任を裁判所が認めたため、認定された損害額から3割を過失相殺しています。
昨今、バグバウンティやCTFのような、高度な脆弱性診断やペネトレーションテストを実施することで富と名声を得るエンジニアが増えているため、テストフェーズでのセキュリティばかりが注目されがちです。しかし、要件定義や外部(基本)設計が根本的に脆弱であれば、どんなにコーディングを頑張ってもセキュアなアプリにはなりません。
ここでは、次の4つの開発フェーズを取り上げ、フェーズごとに考慮すべきセキュリティについて説明します。
- 要件定義・設計
- 実装
- テスト
- 運用
特定非営利活動法人日本ネットワークセキュリティ協会の日本セキュリティオペレーション事業者協議会(ISOG-J)のセキュリティオペレーションガイドラインWG(WG1)と、OWASP Japan主催の共同ワーキンググループである「脆弱性診断士スキルマッププロジェクト3」が、『Webシステム/Webアプリケーションセキュリティ要件書4』という、ウェブアプリケーションを安全に構築するために必要な要件定義書の雛形を公開しています。
この雛形は次の目的のために作成されました。
- 開発会社・開発者に安全なWebシステム/Webアプリケーションを開発してもらうこと
- 開発会社と発注者の瑕疵担保契約の責任分解点を明確にすること
- 要求仕様やRFP(提案依頼書)として利用し、要件定義書に組み込むことができるセキュリティ要件として活用していただくこと
※Webシステム/Webアプリケーションセキュリティ要件書:P2より引用
プログラム言語とそのバージョンによっては、セキュリティ的に危険な実装を避けられない場合があります。
以前、「例えばPHPを避ける」というパワーワードがセキュリティ業界でブームになった時期がありました。古いバージョンのPHPの言語仕様がセキュアではないために、息を吐くように脆弱性を吐き出すことが多かったためです。
アプリケーションフレームワークもプログラム言語と同様の問題を抱えています。Javaで実装されている「Apache Struts2」は、次から次へと致命的な脆弱性が公開されました。以下は公開された脆弱性の一覧を掲載しているサイトです。
「Apache Struts2 の脆弱性対策情報一覧:IPA 独立行政法人 情報処理推進機構5」
セキュアな要件定義および設計が完了したらいよいよ実装フェーズに突入します。しかし、セキュリティを考慮した「セキュアコーディング」を実施しないと、せっかくのセキュアな設計も元の木阿弥です。
プログラム言語やフレームワークそれぞれに固有のセキュアコーディングのスタイルがあります。網羅的にセキュアコーディングを解説するには紙面が足りないため、ここではセキュアコーディングに役立つ参考資料をお伝えします。
IPAは経済産業省所管の独立行政法人です。正式名称は「独立行政法人情報処理推進機構(Information-technology Promotion Agency)」といいます。この組織が2006年1月31日に第1版を公開した「安全なウェブサイトの作り方」は、日本のアプリケーション開発者がセキュリティを考え始めたときに最初に読むべき文書だと思います。現在は改訂第7版6が公開されています。
別冊として「安全なSQLの呼び出し方7」もあります。
どちらの文書も多少古いのは否めませんが、普遍的なセキュアコーディングの考え方を学ぶには、内容および分量的に読みやすいのでオススメです。
JPCERT/CC(Japan Computer Emergency Response Team Coordination Center)は技術的な立場における日本の窓口CSIRTです。「JPCERT コーディネーションセンター セキュアコーディング8」サイトでは、セミナーやセキュアコーディングスタンダードなどの資料が公開されています。
言わずと知れた「徳丸本」(体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践 第2版9)なしではセキュアコーディングは語れません。書名にあるとおり、安全なWebアプリケーションの作り方を具体的なPHPコードで示しています。さらに、付録の脆弱性検証用仮想マシンを使用して、脆弱性の検出方法や具体的な被害などを学ぶこともできます。
ソフトウェアのセキュリティの改善に焦点を当てた国際的な非営利団体であるOWASP10(Open Web Application Security Project)は、セキュアコーディングの考え方や手法を解説する「OWASP Secure Coding Practices - Quick Reference Guide11」を公開しています。
このQuick Reference Guide内にある「Secure Coding Practices Checklist」を活用することで、脆弱性が入り込みにくいシステムを構築することが可能となるでしょう。
セキュリティのテストといえば「脆弱性診断」です。ところで、そもそも「脆弱性」とは何でしょうか。
次の文章は「総務省 安心してインターネットを使うために 国民のための情報セキュリティサイト12」より引用しました。
脆弱性(ぜいじゃくせい)とは、コンピュータのOSやソフトウェアにおいて、プログラムの不具合や設計上のミスが原因となって発生した情報セキュリティ上の欠陥のことを言います。
「プログラムの不具合」であればテストして修正すればよいだけの話ですが、現実には世界中で毎日のように脆弱性が報告されています。理由はいろいろ考えられますが、脆弱性のテストが難しい、というのも理由のひとつでしょう。
脆弱性診断には大きく分けて「自動診断」と「手動診断」のふたつの手法があります。 どちらか一方が良い、というわけではなく、アプリケーションの性質や状況によって適切に使い分けるべきです。
自動診断は、診断ツールを使用して機械的にアプリケーションに対して診断用文字列を送信することによって脆弱性を発見します。人間が実施すると何時間もかかるような操作を数分で実施できる場合があるため、テスト工数の大幅な削減に役立ちます。しかし、「機械的に」実施するため、存在しているはずの脆弱性を漏らしたり、脆弱性とは言えないものを脆弱性だと過検知(False Positive)したりしやすくなります。
手動診断は人間がコツコツと診断用文字列をアプリケーションに送信して実施します。自動診断よりも時間がかかりますが、機械的な操作が困難な画面遷移処理に潜む脆弱性を見つけることができます。
通常はサードパーティーのセキュリティベンダーに脆弱性診断を依頼しますが、最近は、開発者自らが脆弱性診断を実施する「脆弱性診断内製化」の機運が高まっています。 セキュリティベンダーに依頼すると診断工数と費用が嵩みます。そのため、内部で脆弱性診断を実施したいという欲求が生まれるのは自然なことです。しかし、脆弱性診断内製化をすすめるためには、現在開発している言語やフレームワークに対する知識に加えて、アプリケーションに対する攻撃手法や脆弱性診断の方法などを学ぶ必要があるため、相当な情熱がなければ内製化を推進することができないでしょう。
アプリに対して脆弱性診断を実施する場合、アプリのすべての画面や機能を診断するのが理想です。しかし、リリーススケジュールや予算の都合により理想通りに進められない場合があります。
診断対象すべてを診断できない理由ベスト3
- 予算の都合
- 工期の都合
- 政治的都合
セキュリティベンダーにすべての画面を診断できない事情を「正直に」伝えれば、ほとんどのベンダーは「良きに計らって」診断対象を選定してくれるはずです。なお、セキュリティエンジニア目線では、次の優先順位で選定することが多いです。
- 1.認証系
- ログイン/ログアウト
- パスワード処理(変更や再設定など)
- 利用者情報編集(管理者や一般ユーザなど)
- 新規会員登録
- 2.購入/決済系
- クレジットカード情報編集
- 商品購入フロー
- 3.上記以外の編集系処理
- ブログ記事編集
- テーマ編集
意外に難しいのが、診断対象の考え方です。まだ診断対象となるウェブアプリケーションが完成していない場合、ウェブサイトのワイヤーフレームだけをセキュリティベンダーに提示して見積もりをお願いすることがありますが、セキュリティベンダーは概算しか出せないことがほとんどです。見た目の画面数と診断が必要な画面数は異なります。
忘れがちというかうっかりしがちなのが、診断対象の環境についてです。新たに用意した診断専用環境に対して脆弱性診断を実施するのが理想です。診断専用環境であれば、最悪の状況が発生しても業務継続に影響しないためです。
プロダクトの品質を保たせるためには言語やフレームワークのバージョンを最新に保たせる事は大事です。 利用している言語やフレームワークのバージョンの状態を公式サイトやgithubをwatchして定期的にバージョンアップを行いましょう。
利用している全ての依存性のバージョンを監視するのは骨が折れます。 こう言った課題を解決するために、自動でバージョンを更新してくれるサービスがあります。 その一例を紹介するので是非利用してみてください。
- Greenkeeper npmライブラリの自動更新ツールです。
- circleci-bundle-update-pr gemライブラリの自動更新ツールです。
- Angular cli
npmの
@angular/cli
をインストールすると、ng update
コマンドでangular関連のライブラリのアップデートができます。
運用しているプロダクトがwebアプリケーションの場合、リソースの更新に気を使う必要がある場合があります。 ブラウザにはキャッシュシステムが備わっていて同名パスから画像等のソースを取得する際、2回目以降はサーバからではなくキャッシュから読み込むことを行います。 これによりパス名が変わらないとロゴの画像を変更してデプロイしてもキャッシュの有効期限が切れるまで古いロゴ画像をユーザは見続ける問題が起きたりします。 このような問題の解決に、webpackを利用している場合url-loaderというプラグインを使うことで回避することができます。
url-loaderで必ずファイル名を置き換えるのは管理としては安全になりますが、firebase hostingなどファイル転送量で金額をスケールするサービスを利用している場合は注意が必要です。 特に静的サイトジェネレーターを利用するプロダクトだと、他のプロダクトに比べてデプロイ頻度がより多くなることが見込まれます。 もし仮に毎日記事が作成されて毎日デプロイされるということが起きた場合、毎日キャッシュするためにリソースを取り直すことが起き、転送量が想定以上に嵩むなんてことが起きるかもしれません。 Gatsbyではデフォルトでリソースにハッシュ値をつけたファイル名へ置き換える設定になっているので、ファイル名パターンを変更させた方が良いかもしれません。
開発者視点でセキュリティを考える際に重要となるのは、機密性(Confidentiality)、完全生(Integrity)、 可用性(Availability)の3つです。 よくCIAと略されることも多いこの3要素についてざっくりと解説します。 また、近年はクラウドサービスの利用がほぼ必須となってきていますので、クラウドサービスに関するセキュリティ、 サーバーレスアーキテクチャに関するセキュリティ、ソースコードレベルのセキュリティについても言及します。
機密性とは、情報にアクセスできるユーザーを正確に制限できている、ということです。 例えば、Aさんの個人情報はAさんしか閲覧できないようにすべきですが、何かしらのバグでAさんの個人情報が他の人にも見える状況となっていた場合は、 セキュリティインシデントとなります。
実際のデータはデータベースに保存されていることがほとんどかと思います。 データベースのクエリ操作にバグがあると、別のデータを参照できてしまう可能性があるので、 実際のサービスでは、詳細に確認するポイントです。 例えば、田中さんのデータを検索したときに、別の田中さんのデータを検索してしまい、別の人の個人情報が表示されてしまう、 なんてことも考えられます。
完全性とは、その情報が本当に正しいのか、ということです。 例えば、Webサイトが乗っ取られて、内容が不正なサイトへのリンクに書き換わっている場合は、 完全性が損なわれている、という状況になります。
WordPressなどのCMSなどでは、脆弱性を利用して、内容を改ざんされる事例が何点か確認されています。 CMSなどに関しては、最新のセキュリティパッチを適用する、ということが非常に重要かと思います。 また、全てに共通していることですが、アカウントの管理も非常に重要です。 簡単なパスワードを設定しており、CMSなどに不正にログインされると、パスワードを変更されログインできなくなり、 内容を好きに変更されてしまいます。
必要なときにシステムが利用できるか、ということです。 例えば、DDOS攻撃にあってシステムがダウンし使用不可、となった場合は、可用性が損なわれている、 という状況になります。
実際のサービス開発では、サービスの負荷分散、2重化、バックアップといったことを考慮する必要があるかと思います。 AWSの場合ですと、オートスケールというサービスを用いて、動的にスケールアップすることが可能ですし、 データベースの定期的なバックアップを実施することも可能です。 サービスが常に利用できる状態にしておくことも重要ですが、万が一なにか障害が発生した場合に、 迅速にサービスを利用可能な状態に復元することも重要です。
最近はアプリケーション開発において、クラウドサービスの利用は必須と言っても過言ではありません。 クラウドサービスは、オンプレミスとは違い、どこからでもアクセスできてしまうという側面を持っていますので、 注意しなければならないことがたくさんあります。
クラウドサービスへのアクセス管理に関して、多要素認証は必須です。 多要素認証とは、パスワードに加えてワンタイムパスワードなどの追加の認証を設けることです。 万が一、使用しているクラウドサービスに不正ログインされると、甚大な被害を受ける可能性が非常に高くなります。 不正にサービスを利用され、多額の請求を受ける、運用しているサービスを踏み台に、マルウェアを配布される、、といった ことが考えられます。
また、最小限の権限しか与えない、という考え方も重要です。 開発者ではない人間に、全てのサービスへのアクセス権を与えると、その人に悪気がなくても、 間違ってサービスを削除できてしまいます。 必要な権限しか与えず、追加の権限が必要になったタイミングで、権限を付け足していく、という運用が理想です。
クラウドサービスは便利な半面、危険も多くあります。 例えば、AWSのEC2を立ち上げて、sshのログインを有効にしたとします。 この時点で全世界の人間が、作成したEC2インスタンスにssh可能な状態になります。 もちろん、公開鍵認証をかけている場合は、秘密鍵を持っている人しかログインはできませんが、 ログインを試すことは可能です。
「クラウドサービスはどこからでも、誰でもアクセスできる」という前提を理解した上で、適切な対策を取る必要があります。 EC2の例の場合は、パスワード認証は使わず公開鍵認証のみ許可する、sshのポート番号を変更する、sshのアクセス元IPアドレスを制限する、 といった対策が考えられるかと思います。
クラウドサービスを利用する上で、「責任共有モデル」という考えは非常に重要です。 責任共有モデルという単語自体は、AWSの用語なのですが、考え方はすべてのクラウドサービスに通用する考え方となっています。 責任共有モデルとは、クラウドサービス事業者(AWSやMicrosoft、Googleなど)の責任範囲と利用者の責任範囲を明確に区別し、 それぞれの担当範囲にのみ注意してセキュリティを考えれば良い、という考え方です。
上記の図を参照しながら具体的に説明します。 まず、AWSの責任範囲は、リージョンやハードウェア、ネットワークなど、低レイヤーの部分になります。 つまり、利用者はリージョンを管理するデータセンターの面倒はみなくてもいいですし、 データセンター間のネットワークについて考える必要はありません。 データセンター自体の障害は、データセンター間のネットワークになにか障害が発生した場合は、AWSの責任、ということになります。
逆に利用者の責任範囲は、オペレーティングシステム(OS)、アプリケーション、保有しているデータ、といった上位レイヤーの部分になります。 例えば、OSのパッチの適用をしなかったために発生した障害や、アプリケーションのバグ、保有しているデータの不整合などは、 利用者側の責任ということになります。
クラウドサービスを使用しているので、なにか問題があればクラウドサービス事業者の責任だ、という考えではなく、 利用者の責任範囲がどの部分であるかを理解した上で、適切なセキュリティ対策を実施することが重要です。
みなさんサーバーレスアーキテクチャ、マイクロサービスアーキテクチャという単語をご存知でしょうか。 最近のWebアプリケーション開発において、非常に有名な設計手法となります。 サーバーレスアーキテクチャとは、サーバーを意識することなく使用できるサービスを利用して、 アプリケーションを作成するという設計です。 また、マイクロサービスアーキテクチャとは、アプリケーションを一枚岩のように設計するのではなく、 複数のサービスを組み合わせてアプリケーションを設計する、という設計手法になります。 個人的に一番有名だと感じているAPI Gateway + Lambda + DynamoDBについて少し説明して、 それぞれに関する注意事項について言及します。 (AWSのサービスを例にとっており大変恐縮ですが、概念自体は他のクラウドサービスでも通用するかと思います。)
まず、上記の図で使用しているサービスの概要を説明します。
API Gateway
APIの作成をサポートするマネージドサービスです。 マネージドサービスとは、バックエンド(どのようなサーバーで動いているのかなど)を意識することなく、利用できるサービスのことです。 通常、APIを作成するためには、サーバーを作成して、もろもろの設定をする必要がありますが、API Gatewayを利用すると、サーバーの作成、 ネットワークの設定などは全て不要です。 コンソール画面からボタンを押すだけで、APIを作成することが可能です。
Lambda
Lambdaは複数の言語(PythonやNode.jsなど)のコードを実行する事ができるサービスです。 Lambdaももちろんマネージドサービスですので、Lambdaの実行環境などについて意識する必要はありません。 例えば、API Gatewayは、APIの口のみを作成するサービスなので、APIが呼ばれたあとのロジックは、Lambdaが担当することになります。 ですので、特定のAPIが呼ばれると、このような処理をさせたい、という場合には、処理内容をLambdaで実装することで、実現することが可能です。
DynamoDB
NoSQLデータベースのサービスになります。 こちらもマネージドサービスですので、バックエンドを意識する必要はありません。 柔軟にスケールすることが可能となっており、画面からキャパシティの変更が簡単に行なえます。
では、API Gateay + Lambda + DynamoDBの実際のユースケースを見てみましょう。 Webページからユーザーの設定情報をREST API呼び出しで、API Gatewayに対して送られて来たとします。 送られていた情報はLambda関数に渡りますので、Pythonコードでデータの処理や例外処理を実施し、その後DynamoDBのテーブルに データを書き込む、という流れです。
マイクロサービスにおいては、それぞれのサービス自体と接続するサービスの連結部分に着目することが重要です。 まず、API Gatewayに関して必要な設定を見直します。 APIに認証をかける場合は、どのような認証をかけるのか、APIに適切な認証がかかっているかどうか、などを確認します。
次に、Lambdaに関しては、Lambdaに最低限の権限があたっているかどうか、データベースの書き込み部分が、 想定しているレコード書き込みのみになっているかどうかなどを確認します。 Lambdaに強い権限を与えてしまうと、コードの処理を間違えたときに、別テーブルにデータが書き込まれたり、 データが削除されたりしてしまいます。
次にDynamoDBですが、スパイクに耐えることができるように、キャパシティの設定を行ったり、バックップの設定を行うことで、 万が一の場合に備えることができます。
マイクロサービスアーキテクチャにおけるセキュリティは、上記で述べましたように、基本的にはそれぞれのサービスと、 サービスごとの連結部分にのみ、注力すれば良いということになります。(もちろん例外もありますが) 一枚岩のシステムの場合は、コードのロジックが複数の領域に渡っている場合もあり、影響範囲の調査が非常に分かりにくい、 という特徴があります。 ですが、マイクロサービスアーキテクチャ+サーバーレスアーキテクチャの場合は、API自体、Lambda関数自体は独立して存在するものなので、 問題がある場合も、切り分けがしやすいですし、セキュリティを考える上でも、シンプルに考えることができます。
ソースコードの実装に関しては、ユースケースごとに考慮すべき内容が異なりますので、全体的な内容のみを述べることにします。
人間はいついかなる時も、ミスをする生き物です。 個人情報や本来公開してはならない情報が、間違ってソースコード内に含まれてしまう可能性は十分にありえます。 その際に、パブリックリポジトリの場合は、一度プッシュしてしまうと取り返しがつかないのですが、プライベートリポジトリだと、 大事故になることを防ぐことができます。 よほどのことがない限りは、プライベートリポジトリを利用するようにしましょう。
AWSなどのクラウドサービスを利用する場合は、認証キーを用いてサービスにアクセスする必要がありますが、 その認証キーをソースコード内に含めることはしないようにします。 例えば、Lambda関数の場合は、Lambdaにアタッチするロールに必要な権限をもたせることで、コード内に認証キーをもたせることなく、 権限を付与することができます。 また、ローカルで実行するソースコードに関しても、AWS SDKの場合は、AWS CLIで設定するクレデンシャルを参照することができますので、 ソースコード内に認証キーを埋め込む必要がありません。 EC2のようなサーバー上で動作するアプリケーションであっても、EC2にアタッチするロールにAWS Secrets Managerへの読み取り権限を付与すれば、 アプリケーションのコード内に認証キーを埋め込む必要がありません。
基本的なユースケースでは、コード内に認証キーを埋め込むことなく、要求するビジネスロジックを実現可能かと思います。 もちろん、例外もあるのですが、本当にコード内に認証キーを持たせるしか道はないのか、をもう一度見直して頂ければ幸いです。
Webアプリケーション(ブラウザ上で動作するアプリケーション)のコードは、ブラウザの検証ツールを使うと、 誰でも簡単にソースコードを読むことができます。 逆に、WindowsやMac OS専用のネイティブアプリケーションのソースコードは基本的には、読むことができません。 (バイナリをハックすれば、ある程度は可能かもしれませんが、難易度が非常に高いです) なので、Webアプリケーションを開発される方は、ネイティブアプリケーションの開発よりも、更に注意を払う必要があります。 Webアプリケーションのソースコードを見れば、どのタイミングでどのAPIのエンドポイントに対して、どのようなデータを送信しているのかが、 簡単にわかります。 なので、処理にもし不備があれば、その不備をついて攻撃することが可能です。 エンドポイントに対してDOS攻撃を仕掛けることもできますし、同じようなロジックでコードを実装して、 APIに対して偽装リクエストを投げることもできてしまいます。
ソースコード内に認証キーが含まれていなくても、過去のコミット履歴内に認証キーが含まれている可能性があります。 なので、後で消すから大丈夫、開発時のみしか使わないから大丈夫、という考えは危険です。 Gitの履歴を遡れば、すべての内容を確認することが可能なので、開発時であっても、コード内に秘密の情報を埋め込むことは厳禁です。
Footnotes
-
「SQLインジェクション対策もれの責任を開発会社に問う判決 | 徳丸浩の日記」 https://blog.tokumaru.org/2015/01/sql.html ↩
-
https://www.owasp.org/index.php/Pentester_Skillmap_Project_JP ↩
-
https://www.owasp.org/index.php/OWASP_Secure_Coding_Practices_-_Quick_Reference_Guide ↩
-
http://www.soumu.go.jp/main_sosiki/joho_tsusin/security/basic/risk/11.html ↩