暑い日が続きますね。こんにちは @kalibora です。
よその会社であったとしても、セキュリティ関連の事故を見聞きするたびにプログラマーとしては胃が痛くなるのではないでしょうか。
はたして自分のところは大丈夫だろうかと。完璧なんてありえないし、どこかでうっかりミスをして大事故を起こさないだろうかと。
さて、最近では noteでのIPアドレス漏れ のようなことがありましたし、過去には 狙われた7pay「外部ID連携」の脆弱性の全貌。急遽“遮断”した理由 | Business Insider Japan のような問題もありました。
どちらも詳細な原因は外部の私からは知ることはできませんが、APIから不必要なレスポンスを返してしまうことによって起きるセキュリティ関連の事故というのは、わりとよくあるケースなのかもしれません。
私の担当するシステムとしてもこのようなことが起きないか、なにか今以上に対策できることはないか考えてみました。
そもそも何が問題か?
認証のない公開されたAPI(ここでいう公開とは一般に広く仕様を公開しているか否かではなく、認証がなく誰からでもアクセスされる可能性のあるものを指します)というのはWebサーバーがhtmlを返すことと同じなので、 Webページで表示できるものと同じものしか返してはいけない。というのが大前提です。
ですので、公開APIで不必要にセンシティブなデータを返してはいないか?をチェックできればよさそうです。
弊社システムの場合
弊社システムではAPIの仕様(ドキュメント)の生成にOpenAPI(Swagger)を使っています。
この仕様にはAPIのパスごとに認証の有無や返却するレスポンスの定義が含まれているので、 これをしっかりとレビューしていれば問題なさそうです。
また、このOpenAPIの仕様を書くにあたって、Swagger Editor 等のエディターを使うのではなく、 zircote/swagger-php: A php swagger annotation and parsing library を用いて、 ソースコード中のエンティティやバリューオブジェクトへ annotation を付与することで、OpenAPIの仕様を書いています。
さらに、独自実装したシリアライザーを用い、これと同じ annotation を読み取ってレスポンスのjsonを返す。 といったこともしています。
これにより、OpenAPIで記述した仕様と実際に返却されるAPIのレスポンスに乖離が起きることがないため、 やはりOpenAPIの仕様をきちんとレビューすればこのような事故を防ぐことができそうです。
このあたりの詳細については下記のスライドを参照ください。 speakerdeck.com
仕様をチェックするだけなら機械的にレビューできる
OpenAPIの仕様はyamlかjsonで記述されています。ということは、これをプログラムで読み取ることで機械的にレビューできそうです。
例示してみます。
下記のOpenAPI仕様には、オーディオブック取得と自分自身のユーザー情報を取得する2つのAPIがあります。(分かりやすいようにyaml中にコメントを入れています)
paths: /audiobooks/{audiobookId}: get: summary: "オーディオブック取得API" parameters: - name: "audiobookId" in: "path" required: true type: "integer" format: "int64" # レスポンスはdefinitionsに定義したAudiobookを返すということが分かる responses: "200": schema: $ref: "#/definitions/Audiobook" /users/me: get: summary: "自分自身のユーザー情報取得API" # レスポンスはdefinitionsに定義したUserを返すということが分かる responses: "200": schema: $ref: "#/definitions/User" # OAuth認証のあるAPIだということが分かる security: - oauth: ['dummy'] definitions: # オーディオブック情報の定義(タイトルや説明文は公開情報なので、これらは一般公開してよい) Audiobook: type: "object" properties: id: type: "integer" format: "int64" title: type: "string" description: "オーディオブックのタイトル" description: type: "string" description: "説明文" # ユーザー情報の定義(メールアドレスがあったり、基本的に他人からは知られたくない情報) User: type: "object" properties: id: type: "integer" format: "int64" name: type: "string" description: "ユーザー名" email: type: "string" description: "メールアドレス"
/audiobooks/{audiobookId}
のオーディオブック取得APIは、認証がないので公開APIですが、レスポンスのAudiobookが公開できる情報なので問題ありません。
対して、
/users/me
のユーザー情報取得APIが返すレスポンスはメールアドレスがあったりと、一般公開するものではないですが、OAuth認証によって自分自身しか取得できないようになっているため、これもまた問題ありません。(メールアドレスなどの個人情報といえども、APIとして返せないとWebやアプリで表示することは出来ないので、レスポンスに露出していること自体がダメなわけではなく、認証せずに他人の情報が見れることが問題)
ここで、オーディオブック取得APIの仕様が拡張され、下記のようになったらどうでしょうか?
paths: /audiobooks/{audiobookId}: get: summary: "オーディオブック取得API" parameters: - name: "audiobookId" in: "path" required: true type: "integer" format: "int64" responses: "200": schema: $ref: "#/definitions/Audiobook" definitions: Audiobook: type: "object" properties: id: type: "integer" format: "int64" title: type: "string" description: "オーディオブックのタイトル" description: type: "string" description: "説明文" # このオーディオブックを最後に購入したユーザーを返すようになった latestPurchasedUser: $ref: "#/definitions/User"
オーディオブック取得APIは公開APIであるにも関わらず、レスポンスのオーディオブック内の latestPurchasedUser
がユーザーを返すようになったので、ユーザーのメールアドレスや名前などが露出してしまいます。これは問題です。
レビューで仕様をじっくり確認していればこのようなことは防げますが、プログラムで簡単にチェックすることもできます。
今回の場合はそもそも definitions に定義された User が公開APIのレスポンスに現れること自体がまずそうなので、
このyamlファイルをパースし、 $ref は適宜再帰などを用いて解決し、認証のないAPIで User がレスポンスに現れることがないかをチェックすれば問題なさそうです。
他にも properties に特定のキー(passwordなど)が含まれていたらエラーにすることも簡単にできそうです。
ということで、弊社でも早速その様なスクリプトを書いてCIに組み込むことにしました。
汎用性のないソースコードなので特に公開はしませんが、200行くらいの簡易なスクリプトでチェックすることができるようになりました。
まとめ
- 認証のない公開されたAPIはWebと同じ様に扱い、センシティブなデータが返却されないようにすること
- オトバンクではAPIの仕様と実装に乖離がないように保たれているので、APIの仕様をきちんとレビューすればよかった
- APIの仕様をレビューするだけであればすべてのAPIに対しテストを書かずとも、機械的にレビューすることができ、CIに組み込むことが出来た
GraphQLなど他の形式であってもAPIのスキーマを定義しているシステムなら同じようなことができると思うので、みなさまもちょっとしたスクリプトを書いて安心感を増やしてみてはいかがでしょうか。