はじめに
本記事は、オライリーから出版されている「Web API The Good Parts」を私が読んだ中で、初めて知ったという知識・知見をつらつらとメモ書きしていったものであり、読んだ時点で私が既に知っていた知識に関しては記述していません。ご了承ください。完全に自身にあてた復習ノートの位置づけなので万人に参考になるものではありません。
もし本記事で私同様「知らなかった」知識があった場合は、ぜひ実際に本を手に取って読んでみてください。
1章 Web APIとは何か
省略
2章 エンドポイントの設計とリクエストの形式
エンドポイント(URI)は、大文字小文字の混在はNG
ドメイン部分は大文字・小文字の区別はないが、パス部分では大文字・小文字が区別される。/users/と/Users/は、全く別のURIとして区別される。さて、この2つのURIをどのように扱うべきか。様々な方法があり、①「同じパスとして扱う」②「小文字のURIにリダイレクトさせる」③「大文字URIは404エラーを返す」が挙げられていた。SEOの観点では(APIは無関係であるが)、②がよさそう。ただ、有名サービスでは③が目立つ。
Googleクローラにデータを削除される
GETメソッドでサーバ側のデータを変更できてしまうような設計はNGであるが、そういう設計にしていたらGoogleクローラに全てのデータを削除されたというジョークはくすっと来た。
X-HTTP-Method-Overrideヘッダと_methodパラメータ
HTTP0.9の時代から存在したGET, POSTとは異なりPUTやDELETE, PATCHなどのメソッドは環境により使用できない場合がある。GETとPOST以外のHTTPメソッドをPOSTで表現する際に使用する。
URIにケバブケースを使用するべき理由
SEO的観点から「Googleがハイフンは単語のつなぎとみなしているが、アンダースコアは一続きの単語として解釈する」ことが起因しているらしい。ほかにもアンダースコアはAタグのリンクで見にくいなどの理由もある。そもそも極力単語は/で区切るべきである。
相対ページネーション
クエリパラメータにpageなどを指定して巨大なデータの一部を取得する設計(ページネーション)について深く言及されていた。
page/per_page … pageは1から始まるページ値。per_pageは1ページ当たりのアイテム数。
offset/limit … offsetは0から始まるスタート値。limitは取得するアイテム数。
ex) 1ページ50アイテム存在した場合での3ページ目(101アイテム目から)
per_page=50&page=3
limit=50&offset=100
絶対ページネーション
上記の相対ページネーションでは、更新頻度が高いデータにおいて不整合が生じる。そのため、絶対位置をクエリパラメータに指定する手法もある。これは「…から数えて何件目」というのではなく、指定したIDや日にちよりも前・後という数え方をするページネーション手法である。
クエリパラメータとパスの使い分け
そのパラメータをパスに入れたときに以下の条件に当てはまる場合、クエリパラメータに指定してあげるべきである。
そのパスが一意なリソースを表していない
省略可能である
自分の情報に対するパスのエイリアス
/users/:id
というパスがあったとき、自分の情報を取得するときにわざわざ自分のIDを指定して取得するのはめんどくさい。そこで「:id」の部分を「me」や「self」というエイリアスで代替してあげる。そうすることで返されるデータが自身の情報(例えばEmailアドレスを含む)であることは明確だし、認証が必須なことも明らかである。
LSUDsとSSKDs
LSUDs(large set of unknown developers)とは、未知のたくさんの開発者という意味であり、LSUDsをターゲットにしたAPIは、パブリックであるために汎用的でとにかく万人にわかりやすい設計にしなくてはならない。ここまでのプラクティスは、LSUDs向けのAPIに対してである。
対してSSKDs(small set of known developers)とは、既知の少数の開発者という意味で、SSKDsをターゲットにしたAPIは、例えば自社でのみ使用するクライアントアプリのAPIなど、使う人が限られたものである。もちろん汎用的なわかりやすさも重要だが、また別の効率を重視したほうが美しい設計になる。例えば、「1スクリーン1APIコール、1セーブ1APIコール」。
HATEOASとREST LEVEL3 API
REST APIには設計レベルが存在する。LEVEL 0(HTTPを使用)、LEVEL 1(リソース概念)、LEVEL2(HTTPメソッド)、LEVEL3(HATEOAS)。
HATEOAS(読みはヘイタス?)とは、このREST LEVEL3に登場する概念であり、レスポンスの中に次に行う行動、取得するデータ等のURIをリンクとして含める設計のことである。最初のエントリーとなるURIさえ把握していれば、リンクにより全ての情報にアクセスすることが理論上可能。これはネイティブアプリなどの変更が困難なプロジェクトに対し、URIの変更などの急な仕様変更にも耐えることができるメリットがある。また、改造可能(ハッカブル)でないURIを使用でき、セキュリティの観点で簡単に人間にアクセスしてほしくないURIを使用できるというメリットもある。
ただ、これはLSUDsをターゲットにしたAPIに導入するのは時期尚早である。
第3章 レスポンスデータの設計
データフォーマットとその指定方法
APIのデータフォーマットとして、JSON, XML, PHPserialize, MessagePackなどがある。XMLは、AJAX(Asynchronous JavaScript + XML)やXMLHttpRequestなどからわかるようにかつてのデファクトスタンダードである。しかし、現在ではそのシンプルなわかりやすさからJSONが最も普及している。
データフォーマットの指定方法は様々で、クエリパラメータで「?format=xml」などのように指定する方法がわかりやすいという理由で普及している。Acceptヘッダに指定する方法は、最もお行儀が良いが少し敷居が高いことがネック。/users.json
などのように拡張子で指定する方法もあるが、あまり使われていない。
JSONP
JSONP(ジェイソンピー)は”JSON with padding”の略で、通常はJSONをJavaScript(padding)でラップして返すデータ送信方法である。
例えば、/users/123?callback=showUser
というURIにリクエストした場合、以下のようなJavaScriptを返す。
showUser({"id": 123, "name": "kensan"})
なぜこんな回りくどいことをするかというと、XHTMLRequestの同一生成元ポリシー規制の回避のためである。JSONPは以下のようにscriptタグで指定しデータを取得する方法である。
<script src="https://example.com/v1/user/123?callback=showUser">
もちろん、実行前にshowUser関数はグローバルに定義してあげる必要がある。JSONPは、セキュリティ上の対策を回避してしまう方法なので、明確な理由がない限りサポートする必要はない。
Chatty API
1つの作業をするために複数回のアクセスを必要とするAPIは”Chatty(おしゃべりな) API”と呼ばれ、面倒くさい印象を与える。
レスポンスの内容をユーザが取捨選択する
Chatty APIは、全ての情報をまとめて返すという力業でとりあえずは解決できる。しかし、必要以上に大量のデータを受け取ることとなり、取得もパースも遅くなってしまう。そこで、ユーザが受け取る情報の取捨選択を行うことで解決する。例えば、「?fields=name,age」をクエリパラメータとして指定し、名前と年齢のみを受け取るということができる。また、そもそもこちらでレスポンスグループ(Amazon命名)を定義し(例. Small, Medium, Large)、それをユーザが指定するという方法もある。
エンベロープ
エンベロープは「封筒」という意味で、APIの文脈でとらえると、全てのレスポンスで共通化されたデータ構造のことを指す。
{
"header": {
"status": "success",
"code": 0,
},
"response": {
...
}
}
ただし、このエンベロープはHTTPを使用している限り冗長であり、採用するべきではない。
JSONのトップレベルは配列か?オブジェクトか?
JSONのトップレベルに配列とオブジェクト、どちらでも置くことが可能であるが、どちらを採用したほうがよいか?以下の理由からオブジェクトが良いとされている。
・レスポンスデータが何を表しているのかkeyを見ることでわかりやすくなる。
・トップレベルを全てオブジェクトでそろえて共通化することでクライアントに優しかったりする。
・JSONインジェクションを防ぐ。JSONインジェクションとは、script要素を利用してJSONを不正に読み込ませる脆弱性である。そのJSONが配列であれば、JavaScriptの文法として正しいため正常に読み込まれてしまう。しかし、オブジェクトであればJavaScriptは{}をブロックと判断しシンタックスエラーを出力してくれる。
ページネーションのレスポンスにおいて、次の情報をどう伝えるか?
以下の2手法が考えられる。
・「hasNext」などのフラグを一緒に返す。
・「次があるか」という情報ではなく、そもそも次のページのURLやパラメータを教えてあげちゃう(HATEOAS)。
フィールド名のスタイル
レスポンスのJSONのフィールドの命名規則の話(キャメルケース or スネークケース)。JavaScriptで使用することを前提とした場合、キャメルケースを採用することが良しと思われるが、実際のサービスではスネークケースが採用される場合も多い。「スネークケースのほうがキャメルケースよりもずっと読みやすいという研究結果」がある。どちらを採用するかは利用状況次第。ただし、API全体で統一するということは忘れずに。
APIにおける性別の表現
「sex」「gender」の2種類のフィールドが考えられる。「sex」の場合、生物学的な性別の意味合いが強く、男性・女性・不明(その他)の3性別ほど。そのため、データ形式としては数値でもよい(ex. 男性:0)。医療系のサービスではこちらを採用すべき。対して、「gender」は社会的・文化的性別の意味合いが強く、上記3性別以外の性別も含まれる。そのため、データ形式としてはわかりやすく文字列で表すことが無難(ex. 男性: “male”)。SNSやEC、その他大部分のサービスではこちらを採用すべき(Facebookはgenderが50種類以上も用意されている)。
レスポンスボディにおける日付のフォーマット
RFC3339(ex. “2015-10-12T11:30:22+09:00″)を採用するべき。理由としては、”Jan”や”Fri”のような特定の言語に依存した表限を含まず、冗長な曜日表記も含まないという点である。SSKDs用のAPIである場合は、数値のみのUnixスタンプで表せば比較や保持が容易でサイズも小さく抑えられる(デバッグはしづらいが)。
JSONにおける大きな整数の取り扱い
JavaScriptなどの言語では、32ビットより大きい数値を処理すると桁あふれを起こす。そのため、巨大な数値は文字列で返すことでそれを防ぐ。
エラー詳細をどのように伝えるか?
APIにおいて、エラーが起きたとき、まずはざっくりとしたエラー内容をステータスコードによって伝える。ただ基本的にはそれだけでは不十分である(/users/:idに対する404エラーでもそのURIが存在しないのか、そのIDのユーザが存在しないのかわからない)。
エラーの詳細を伝える方法として以下の2種類がある。
・独自に定義したヘッダ項目を追加する(ex. X-MYNAME-ERROR-CODE: 2013)。
・ボディ(JSON)に含める。
後者はエンベロープされているように見えるためアンチパターンであると思われるが、実際のサービスのほとんどが後者を採用している。クライアントが使いやすいという意味で後者を採用すべき。
エラー詳細に何を含めるか?
・エラー詳細コード。ステータスコードとは異なるAPIごとに独立したエラーコード。開発者が勝手に決めてよいが1000番台は汎用的なエラー、2000番台はユーザ情報のエラー、などのようにカテゴライズしたほうがわかりやすい。
・開発者向けエラーメッセージとエンドユーザ向けのエラーメッセージ。
・ドキュメントURL
メンテナンス
APIにおいてメンテナンスは利用してくれている他サービスも停止してしまうため極力避けるべきであるが、万が一メンテナンスを実施する場合はきちんと503を返し、Retry-Afterヘッダによりいつメンテナンスが終わるかを伝える。また、メンテナンス時間は多く見積もる。
第4章 HTTPの仕様を最大限利用する
認証エラーと認可エラー
ステータスコード401(Unauhorized)は認証(Authentication)エラーを表し、403(Forbidden)は認可(Authorization)エラーを表す。両者の違いは、401は「あなたは誰だかわからないよ」、403は「あなたが誰だかわかったけどこの操作はあなたには認可されてないよ」。
【キャッシュ】Expiration Model(期限切れモデル)
あらかじめレスポンスデータに保存期限を決めておき、期限が切れたら再度アクセスをして取得を行うキャッシュ方法。期限の指定方法は、Expiresヘッダに絶対時間を指定する方法と、Cache-Controlヘッダにmax-ageで現在時刻からの秒数で指定する方法の2つ。HTTP1.1の仕様によれば1年以上未来の日付を送るべきではないとされている。
【キャッシュ】Validation Model(検証モデル)
今持っているキャッシュが有効かどうかをサーバに問い合わせるキャッシュ方法。期限切れモデルと異なり、必ずネットワークアクセスは発生する。検証モデルを行うには、条件付きリクエストに対応する必要がある。更新されていたらデータを返し、更新されていなかったら304(Not Modified)を返すだけ。クライアントはリクエスト時「現在保持している情報の状態」をサーバに伝える必要があるが、その指標は『最終更新日付』と『エンティティタグ(ETag)』の2種類ある。最終更新日付はLast-Modifiedヘッダに時刻を、エンティティタグはEtagヘッダにハッシュ値を指定する。
エンティティタグには「強い検証」と「弱い検証」があり、「強い検証」は完全一致、「弱い検証」はデータは完全に一致していないが、リソースの意味合いとしては一致しているときなどに用いる(webページなどでアクセスのたびに広告の情報のみ変化するようなリソース)。
HTTP時間
DateやExpiresをはじめHTTPヘッダで時刻を指定することはあるが、HTTPヘッダ内で使用できる時刻フォーマットが決まっている。ちなみに前述したレスポンスボディにおける日付のフォーマットで薦められていたRFC3339形式やUnixタイムスタンプは使えない。RFC1123形式を使うべき。
キャッシュをさせたくない場合
Cache-Controlヘッダにno-cacheを指定する。厳密には「検証モデルを用いて必ず検証を行う」必要があることを意味する。機密情報などをキャッシュしてほしくない場合には、no-storeを指定する。
Varyを用いたキャッシュ単位の指定
Accept-Languageヘッダで指定された国によりレスポンスデータの言語を変えるようなAPIだった場合、URIだけを見てキャッシュをしたとき不整合が起きてしまう(英語で受け取りたいのに以前キャッシュされていた日本語が返ってしまった時など)。そういったときにVary: Accept-Languageをヘッダに指定することでキャッシュ単位を指定することができる。
メディアタイプ
レスポンスではContent-Typeヘッダに指定してレスポンスがどのようなデータ形式かを示す。メディアタイプは「トップレベルタイプ名/サブタイプ名[;パラメータ]」で指定する(ex. application/json; charset=utf-8)。歴史的経緯で昔からtextを採用しているtext/cssとtext/htmlを除くと、たとえテキストデータとしても開くことが可能でも、その形式を知らないと読みこなせないようなデータ形式はトップレベルタイプ名はapplicationとするほうが主流となっている。
x-で始まるメディアタイプ
YAMLを表す「application/x-yaml」やMessagePackを表す「application/x-msgpack」など”x-“で始まるメディアタイプは、IANA(Internet Assigned Numbers Authority)に登録されていないメディアタイプであることを意味する。HTMLのフォームデータを送信する際に用いられる「application/x-www-form-urlencoded」だけは唯一IANAに登録された後も”x-“で始まるメディアタイプである。
しかし、これから独自のメディアタイプを作って配信したいときはこの”x-“で始まるメディアタイプを使用するべきではない。この方法は現在は廃止されていて、いまだに残っているメディアタイプが存在するだけなのである。現在は「vnd.」接頭辞をサブタイプ名に付与することによりベンダツリーのメディアタイプであることを示す。例えば、Excelファイルを示すメディアタイプは「application/vnd.ms-excel」である。他にもパーソナルツリーを表す「prs.」、未登録ツリーを表す「x.」接頭辞がある。
JSONやXMLを用いた独自のメディアタイプ
JSONやXMLを用いて独自のデータ形式を用いる場合は”+xml”などを付けて表す(ex. application/rss+xml)。ただし、この方法はそのデータ形式を知っているクライアントでないとわかりにくく、ライブラリによっては未知のメディアタイプを受け取ったとしてエラーになってしまう。GitHubでは、メディアタイプはapplication/jsonで独自のX-GitHub-Media-Typeヘッダを用いて詳細なデータ形式を指定している。
リクエストにおけるメディアタイプ
リクエストヘッダにもContent-Typeは指定でき、これはリクエストボディのメディアタイプを示す。つまり、POSTリクエストを送る際にデータをJSONで送る時はここにapplication/jsonを指定する。
Acceptヘッダには、クライアントが「どんなメディアタイプを受け入れ可能か」をサーバに伝える。複数のメディアタイプを”,”区切りで列挙可能で、qパラメータ(QualityValue: 品質値)で優先度を指定できる(デフォルト: 1)。また、「*/*」は全てのメディアタイプを表すワイルドカード。
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
このように受け入れ可能なメディアタイプを指定してサーバ側に配信するデータ形式を決めてもらう方法をHTTP1.1ではServer Driven Content Negotiation(サーバ駆動型コンテントネゴシエーション)と呼びます。自然言語を指定するAccept-Languageや文字コードを指定するAccept-Charsetなどがほかにある。
同一生成元ポリシーとクロスオリジンリソース共有
XHTTPRequestでは異なるオリジンにアクセスを行い、レスポンスデータを読み込むことができないという同一生成元ポリシー(Same Origin Policy)というセキュリティ上のポリシーがある。よって通常https://www.example.com/からhttps://api.example.com/へ通信ができない。これを避けるためにJSONPが考案されたが、公式の仕様であるクロスオリジンリソース共有(CORS)を利用するべきである。CORSは特定の異なるオリジンからのアクセスを許可することを可能にする。リクエスト時、クライアントはアクセス元オリジンをOriginヘッダに付与する。サーバはそのオリジンを見て許可されたオリジンであればレスポンスデータを返す。その際、Access-Control-Allow-Originヘッダにそのオリジンを付与する。「*」が指定された場合、どのオリジンからでもアクセス可能であることを意味する(LSUDs用のAPIなどに)。
プリフライトリクエスト
プリフライトリクエストとは、生成元をまたいだリクエストを行う前にそのリクエストが受け入れられるかどうかを事前にチェックするものである。
独自のHTTPヘッダを定義する
既存のHTTPヘッダでは表せないメタデータは独自に定義したヘッダで送信することが可能である(クライアントの表示可能式数やデバイスピクセル等)。
X-AppName-PixelRatio: 2.0
X-GitHub-Request-Id: 719794F7:4A38...
HTTPヘッダは仕様的に大文字小文字の区別はないが、上記のように先頭や単語のつなぎを大文字にし、単語間はハイフンでつなぐのが一般的。かっこや@;は使えない。慣例的に独自のHTTPヘッダは”X-“で始まることが多いが、”X-“はむしろつけるべきではないのではないか、という流れになっているらしい。ただ、「独自に定義したもの」であることが一目瞭然であり、”X-“で始めても問題ないとされている。ただし、付ける付けないはAPI全体で統一するべきである。
第5章 設計変更をしやすいWeb APIを作る
設計変更がWeb APIに及ぼす影響
例えば、レスポンスデータの内容がより詳細になったり(ユーザ情報レスポンスに新たに誕生日フィールドを追加等)、検索の精度がより向上したりする設計変更はAPIにそこまでの影響は与えない。対して、あるIDを数値から文字列に変更したり、「おすすめ関連情報」の機能が廃止されてしまったりした設計変更はAPIに大きな影響を与える。つまり、レスポンスデータの形式そのものが変わった場合、APIに対する影響は大きくなる。
APIの種類によりAPIの仕様変更による影響の大きさは変わってくる。
外部に公開しているAPI(LSUDs)> モバイルアプリケーション向けAPI(SSKDs)> Webサービス上で使用するAPI
いずれにせよ一度公開したAPIの仕様変更は問題発生の危険性がある。
APIのバージョニング
APIのバージョニング手法は以下の3つがある。
1.パスに埋め込む…Facebook, Twitter, ぐるなび, その他多くのサービスが採用し最も普及している手法。”v”を付けないサービスもあるが、バージョンを表していることを明示的に示すため付けたほうが良い。バージョン番号をどこまで含めるかといったところだが、GitHubのファウンダーが提唱するバージョニング手法のセマンティックバージョニングのメジャーバージョンにあたる数値のみ含めるのが良い(そもそもAPIバージョンは頻繁に変えるべきではないし、小さな変更は後方互換性を保つべきであるから)。Amazonや楽天などバージョン番号を日付で表しているサービスもあるが、長くて覚えづらい、古い日付はWeb界において古臭いイメージがあるなどの理由から推奨されていない。
2.クエリ文字列で指定する…NetflixやAmazonなどが採用している手法。クエリ文字列の特徴から省略可能という大きな特徴がある。省略した時のデフォルトバージョンはサービスごとに決めておかなければならず、Amazonは最新バージョン、Netflixは過去のバージョンがデフォルトである。クライアントにとって、ここは混乱の原因となりえるため、パスに埋め込んだほうが無難。
3.メディアタイプで指定する…GitHubやGoogleの各種API(YouTube DATA APIなどいくつかのGoogle APIはパスに埋め込む手法に移行している)などが採用している手法。以下のようにAcceptヘッダに指定する。
Accept: application/vnd.example.v2+json
前述したとおり、独自のメディアタイプではエラーとなるライブラリもあるため、独自のHTTPヘッダを定義してそこでバージョンを指定しているサービスもある。Googleの各種APIではGData-Versionというヘッダにバージョンを指定する手法を採用している。
これらの手法を複数採用しているケースもある(Google Spreadsheets APIはクエリ文字列指定とリクエストヘッダ指定を採用)。しかし、特に理由もなければパスに埋め込む手法を用いることが無難。
できる限りバージョンは上げない
クライアント側はもちろんサーバ側もコストが重なるため、できる限りバージョンは上げない。例えば、genderフィールドに数値を入れて返していたが、文字列を入れたいという仕様変更があるとしたら、genderStrという新たなフィールドを用意し、ドキュメントに明記する(次回メジャーアップデートが行われたら廃止することも)。これにより後方互換性は保たれる。
常に最新版を返すエイリアス
バージョンをクエリパラメータに指定するタイプのAPIやパスに指定するAPIでもそのバージョンを省略した時のリクエストで、最新版にアクセスするような設計が考えられる。将来同じ方法でアクセスして挙動が変わる可能性があるため、そのような設計をする必要はない。
APIの提供終了
Twitterの場合は、バージョン1.1リリースの発表と同時にそれまでのバージョン1.0の廃止を発表。その後、「Blackout Test」という実験的に古いバージョンを一時停止するテストを定期的に行い、合わせて継続的な告知を行った。
廃止時の振る舞いを事前に決めておくことも一つの手。例えば公開終了したエンドポイントにアクセスした時に410(Gone)とエラーメッセージを返す。自社モバイルアプリであれば、これを受け取った時に強制アップデートのアラートを表示するなど。
利用規約にサポート期限を明記する
APIの利用規約にあらかじめ古いバージョンをどれくらいの期間サポートするのかを明記しておく。ただし、ここで明記した期間は変更することが大変難しく、公開する前に十分に注意を払う必要がある。
オーケストレーション層
LSUDs用のAPIであれば、一つのアクションを行うのに複数のエンドポイントにアクセスしなければならなかったり、不要なデータも受け取らなければならなかったりする。このようなAPIを洋服のフリーサイズになぞらえて「one-size-fits-all(OSFA)」アプローチと呼ばれる。
Netflixは、さまざまなデバイスの機能やリリースサイクルに対応するために少しずつ違ったAPIを提供する必要があった。そこでNetflixはOSFAアプローチをやめ、汎用的なAPIとクライアントの間に「Client Adapter Code」を実行する層を挟み、様々な利用状況に対応できるようにした。これをオーケストレーション層と呼ぶ。
第6章 堅牢なWeb APIを作る
サーバ⇔クライアントでの情報の不正入手
喫茶店や公共の場所で同じWifiに繋いている人の通信を盗み見ることは非常に簡単(パケットスニッフィング)。POSTするときのデータに含めるパスワードやIDを盗み見られることはもちろん、セッション情報を盗み見て成り済ますこともできてしまう(セッションハイジャック)。以前FireSheepというFirefoxのプラグインが配信されていて、それを使えば簡単に他人のFacebookなどのセッション情報を盗み取ることができてしまっていた(現在は解決済み)。
HTTPSによるHTTP通信の暗号化
前述したパケットスニッフィングやセッションハイジャックを防ぐ最も普及され簡単に導入できるのが、ご存じHTTPSというTLSによる暗号化である。URIパス、クエリパラメータ、ヘッダ、ボディ(リクエストとレスポンス両方)が暗号化される。
ただし100%安全かというとそうでもなく、様々な要因で安全性が担保されなくなる。まず、ウェブサーバや暗号化機能を提供するライブラリのバグである。2014/04にOpenSSLという暗号化ライブラリのバグが原因でHTTPSで暗号化されているはずの情報が盗み出される可能性が発生してしまうという問題が起きた(Heartbleed)。また、クライアント側でSSL証明書の検証をきちんと行っていない場合にも問題が発生する可能性がある。これは、中間者攻撃(MITM: man-in-the-middle attack)によって通信経路に紛れ込んだ攻撃者が偽の証明書を流し込む危険性がある。
このようにHTTPSを使用していても少なからず危険性は残るが、HTTPSの導入はかなり有効な手段であることは間違いない。
HTTPS通信は、HTTP通信に比べてハンドシェイクに時間がかかるため、わずかにアクセス速度が遅くなるという問題がある。明らかに外に漏れても問題がない通信などはHTTPを利用するなどの手も考えられる。
XSS
XSS(クロスサイトスクリプティング)は、ユーザの入力を受け取ってそれをページのHTMLに埋め込んで表示する際に、ユーザから送られてきたJavaScriptなどを実行できてしまう脆弱性である(こちらの記事内の図が参考になる)。これを許してしまえばセッションクッキーなどブラウザに保持された除法にアクセス可能だし、同一生成元ポリシーをくぐり抜けてサーバにアクセス可能だしとやりたい放題。これは単純なWebサイトだけでなく、Web APIでも起こりうる。したがってユーザからの入力はきちんとチェックすることが必須だし、JSONを返す時にもデータの内容をチェックすることは重要。
JSONなどの形式でデータを返すAPIの場合には、データ(JSON)をブラウザが異なるデータ形式(HTML等)であると解釈してしまうために発生するXSSもある。もし以下のレスポンスデータがContent-Type: text/htmlで返る仕様の時、URIに直接ブラウザでアクセスしたユーザはこのalertスクリプトが実行されてしまう。これを防ぐには必ずContent-Type: application/jsonで返すことが必須。
{"data": "<script>alert('xss');</script>"}
そうすることでモダンなブラウザはJSONとして解釈してくれる。ただし、IEに関してはContent-Typeヘッダを無視しデータの内容からデータ形式を推測する「Content Sniffering」という機能が働き誤ったデータ形式で認識してしまう可能性が出てきてしまう。そこで、X-Content-Type-Options: nosniffをレスポンスヘッダに追加することで「Content Sniffering」を無効にすることができる(IE8以降のみ)。
IE7以前では上記の方法は使用できないので、追加のリクエストヘッダのチェックを行う必要がある。具体的にはX-Requested-With: XMLHttpRequestなどのリクエストヘッダを追加で付与することで、URIに直接ブラウザでアクセスしていないことを伝える手法である。また、JSON文字列のエスケープも有効だ。”<“や”>”, “/(スラッシュ)”などをエスケープすることでこれらを単なる文字列として送信することで、スクリプトを無効にする。
XSRF
XSRF(またはCSRF: シーサーフ)は、クロスサイトリクエストフォージェリ(Cross Site Request Forgery)の略であり、サイトをまたいで偽造したリクエストを送り付け、ユーザが意図せずサーバに攻撃してしまう脆弱性である(こちらの記事内の図が参考になる)。
まずは、サーバ側に影響を与える変更にGETメソッドを用いないことが有効である。ただし、たとえGETメソッドを使わなかったとしてもFORM要素からPOSTメソッドを使って攻撃を仕掛けることができてしまう。そこで、ワンタイムトークン、少なくともセッションごとにユニークなトークン(XSRFトークン or CSRFトークン)を用いて、そのトークンがないアクセスは拒否するという手法が有効だ。他にもクライアントから独自のリクエストヘッダを付与することを必須にする手法もある(現在のFORM要素ではリクエストヘッダをいじれないため)。
JSONハイジャック
JSONハイジャックとは、APIからJSONで送られてくる情報を悪意ある第三者が盗み取る脆弱性である。例えば、悪意あるサイト(example.comでない)のHTMLに以下のscriptタグがあった場合、example.comのユーザ情報を簡単にリクエストできてしまう。
<script src="https://api.example.com/v1/users/me"></script>
ただこのままではJSONがリクエストされるだけで利用はされない。攻撃者は以下のようなスクリプトを事前に実行しておくことでJSONを利用可能な状態にしてしまう。1つ目はFirefox2.0時代に存在していた手法で最近は解決されている。2つ目はFirefox3.0時代のものでいまだにAndroid2.3系では残っている問題である。3つ目は、IE9・IE10では修正されていない。(詳しくは本書で)
<script type="application/json">
var data;
Array = function() {
data = this;
};
</script>
<script type="application/json">
Object.prototype.__defineSetter__('id', function(obj) {alert(obj);});
</script>
<script>
window.onerror = function(e) {
}
</script>
<script src="https://api.example.com/v1/users/me" language="vbscript"></script>
JSONハイジャックを防ぐ方法として、まず同一生成元ポリシーがある。他には、JSONをscriptタグでは読み込ませないようにする(特別なヘッダを必須とすることで)。また、JSONをブラウザにJSONときちんと認識させることも重要だ(Content-Type: application/jsonだけでなく、X-Content-Type-Options: nosniffも)。前述したものであるが、JSONをJavaScriptとして解釈させないことも有効だ(JSONのトップレベルをオブジェクトにしてエラーさせる等)。他にもFacebookが採用している防御策として、JSONファイルの先頭に無限ループなどを仕込んで処理をそこでストップさせるものもある(最後の手段)。
for (;;); {"t": "heartbeat"}
悪意あるアクセスへの対策
今までは悪意ある第三者の話であったが、認証された利用者そのものが悪意を持っていた場合もある。例えば、パラメータの改ざんやリクエストの再送信などを行って、ゲームやECサイト上で自分に有利な状況を作り出すことが考えられる。
セキュリティ関連のHTTPヘッダ
・X-Content-Type-Options…nosniffを指定することでJSONをJSON以外で解釈されることを防ぐ。GitHubでは、JavaScriptファイルを直接読み込ませないためにメディアタイプをtext/plainにしたうえでこのヘッダにnosniffを設定している。
・X-XSS-Protection…ブラウザが備えているXSSの検出、防御機能を有効にするヘッダ。
・X-Frame-Options…指定したページorAPIがフレーム(frameとiframe要素)内で読み込まれるかどうかを制御する。透明にしたiframeでユーザが意図せずクリックし、投稿やランキングに星5を付けたりといったことがある(クリックジャッキング)が、それを防ぐ。同一生成元ポリシーでこれは防げるが、未知なる脆弱性があることを見越してつけておいて損はない。
・Content-Security-Policy…読み込んだHTML内のimg、script、link要素などの読みこみ先としてどこを許可するのかを指定する。
・Strict-Transport-Security…あるサイトへのブラウザからのアクセスをHTTPSのみに限定させる。HTTPアクセスをHTTPSにリダイレクトさせる手法はよくあるが、それだけでは初回アクセスがHTTP通信で安全でない。このヘッダを用いてブラウザに設定すればブラウザ側でHTTP通信を抑制することができる。
・Public-Key-Pins…SSL証明書が偽造されたものでないかをチェックする。
Set-Coolieヘッダとセキュリティ
Set-Coolieヘッダには、Secure属性とHttpOnly属性が指定可能。Secure属性を指定すれば、セキュアでないHTTPでの通信でクッキーを送信しなくなる。HttpOnly属性を指定すれば、HTTP通信のみでクッキーが使われ、JavaScriptなどのスクリプトを使ってクッキーにアクセスできなくできる。
アクセス制限(レートリミット)
大量アクセスのDoS攻撃を防ぐための最も現実的な方法は、ユーザごとのアクセスを制限することである。単位時間当たりの最大アクセス回数(レートリミット)を定め、それ以上のアクセスがあった場合にエラー(429 Too Many Requests)を返す。ユーザの識別方法として、アカウント単位だったりIP単位だったりする場合もある。
エンドポイントで制限値を変化させているケースもあり、高頻度なアクセスが期待されるエンドポイントは制限値を緩くしたりする。例えばTwitterでは、ツイートの検索は180回/15分だが、DMの取得は15回/15分に設定されている。
また、レートリミットの単位時間だが、サービスによって15分でリセットというケースも見受けられるが、1時間くらいが一番多い。
レートリミットをユーザに伝える
ドキュメントに記載することは前提だが、それでは足りないかもしれない。Googleでは、API利用者向けのダッシュボードの中でユーザごとの利用回数や制限を開示している。
GitHubやTwitterでは、レートリミット通知用のエンドポイント(ex. https://api.github.com/rate_limit)を用意している。
その他の方法として、レートリミット情報を各レスポンスに含めるというものだ。今デファクトスタンダードとなっているのは、レスポンスヘッダに「X-RateLimit-Limit」などの項目を追加し通知する手法だ。
コメント