はじめに
この記事は、WebAPI: The Good Partsを読み、後で見返したいことをまとめたものになります。
リクエスト編
クエリパラメータとパスの使い分けについて
WebAPI開発をしているものの、双方の使い分けについて明確な判断基準を持っていなかったためメモ。
書籍内での基準は以下。
- 一意なリソースを表すのに必要な情報かどうか
- 省略可能かどうか
書籍の内容も踏まえて
クエリパラメータが適しているもの
検索条件など省略できるもの。リソースとは無関係なもの。
パスが適しているもの
URIがリソースを表す場合。ユーザーIDなど参照したい情報が一意に定まるもの。
自分の情報へのエイリアス
me
やself
というキーワードをパスに使用し、リソースはアクセストークンの情報を元に取得する。
他人の個人情報を閲覧できてしまうバグを防ぐことができる。
エンドポイントの共通部分について
サービスのドメイン、ドメイン名がexample.com
ならapi.example.com
の形式が一般的。
レスポンス編
レスポンスデータのフォーマット指定方法
3パターンが挙げられる。
- クエリパラメータ
?format=json
などの形式で指定 - 拡張子
URIの最後に.json
,.xml
といった拡張子をつける - リクエストヘッダ
Acceptヘッダで指定する
HTTPの仕様的には、リクエストヘッダで指定するのが最適に思えるが、利用する側のハードルが上がるため、クエリパラメータが無難。
次点でリクエストヘッダ。
JSONP(JSON with Padding)への対応
セキュリティリスクがあるため、必要がある場合のみ対策した上で提供する。
レスポンス内容をユーザーが選択できるようにする
返却する内容が多かったり、ユースケースが想定できない場合は、クエリパラメータで取得する内容を選択できるようにする。
...users/123?friends=name,age
取得したい項目をまとめたグループ名を指定する方法もあり。
エンベロープを使うかどうか
通常はHTTPのステータスコード、ヘッダーがあるため不要。
JSONPの場合は、ステータスコード、ヘッダーへアクセスできないためあった方が良い。
データをフラットにするかどうか
接頭辞を使用してデータを表現している場合は、階層構造にした方がサイズの節約になって良い。
それ以外では階層構造にするメリットはそれほどない。
配列返却時はオブジェクトで包む
レスポンスデータをオブジェクトで統一でき、クライアントでの処理が楽になる。
script要素を使って読み込めるAPIの場合、トップレベルが配列だとJSONインジェクションのリスクがある。
取得したデータに続きがあるかどうか
データ取得件数が決まっている場合、その件数+1で取得することで全件取得せずにデータの続きがあるかどうかの判断ができる。
それを元にレスポンスに続きがあるか判定できる項目(hasNext
)を追加する。
他にも次ページのURIを返却する方法、次ページの取得に利用するパラメーターを返却する方法がある。
性別
生物学的な性別が必要な場合はsex
、設定値は数値。
社会的・文化的性別の場合はgender
、設定値は文字列。
日付のフォーマット
RFC 3339(2022-10-11T12:30:11+09:00
)がデバッグのしやすさや言語依存の表現もなく無難。
エラーメッセージ
開発者向けとユーザー向けのメッセージを用意するのもあり。
ログイン周りのエラーはセキュリティの観点からエラーメッセージは曖昧で良い。
HTTPの仕様を利用する
Dateヘッダ
500番台エラー等の例外を除き、必ずつける。
キャッシュの時間計算やサーバーとの同期でも使用するため。
キャッシュ
プロキシサーバーを挟んでいると意図しないキャッシュを行う可能性があるため注意。
Expiration Model(期限切れモデル)
サーバーからのレスポンスにCache-Control
ヘッダまたは、Expires
ヘッダを付与して期限切れのタイミングをクライアントへ通知する。
クライアントはその情報をもとにキャッシュの有効性を確認する。
それぞれのヘッダの利用シーンとしては、
-
Expires
あらかじめキャッシュの更新タイミングがわかっているとき -
Cache-Control
そこまで更新頻度が高くなく、リアルタイム性が必要ない情報でサーバーのアクセス負荷を減らしたいとき
Validation Model(検証モデル)
クライアントがサーバーに更新の有無を問い合わせる方式。
データ容量が多い場合などに有効。
データが更新されていない場合は、更新されていないという情報のみ、更新されている場合は、更新データを送る。
クライアントが「条件付きリクエスト」に対応する必要がある。
2種類ヘッダを使用したリクエストの方法がある。
-
IF-Modified-Since
ヘッダ
最終更新日付を使用する場合。
サーバーのレスポンスでは、Last-Modified
ヘッダがレスポンスに付く。 -
IF-Non-Match
ヘッダ
エンティティタグ(ある特定のリソースのバージョンを表す識別子)を使用する場合。
サーバーのレスポンスでは、ETaq
ヘッダがレスポンスに付く。
Varyヘッダ
キャッシュするかどうかの判断をURIだけでなく、Vary
ヘッダで指定したリクエストヘッダも含めるためのもの。
サーバーがクライアントへのレスポンス時に付与する。
メディアタイプ
メディアタイプ(Content-Type
,Accept
)は、適切なものをちゃんと付与する。
そうしないとクライアントがデータを正しく認識できなかったり、埋め込まれたJavascriptが実行されるリスクがある。
バージョン管理
バージョン情報はパスに埋め込むのが無難
APIのバージョン情報の埋め込み方はいくつか方法がある。
- URIにバージョンを埋め込む
セマンティックバージョニングのメジャーバージョン番号をURIに埋め込む。 - クエリ文字列でバージョンを指定
省略可能になるが、省略した場合のバージョンが直感的に分かりにくい。 - メディアタイプでバージョンを指定
独自ヘッダでバージョンを指定する方法もある。
HTTPの仕様を利用していて最適と思えるが、直感的に分かりにくい。
以上より、バージョン情報はURIに埋め込むのが直感的に分かりやすく無難。
オーケストレーション層の導入
大規模なサービスの場合、利用シーンが多岐に渡り、必要な情報もデバイスによって異なるため、オーケストレーション層を導入しているところもある。
オーケストレーション層は、クライアント側が開発し、汎用的なAPIから必要な情報を取り出したり、APIを組み合わせて実行するするような実装が行われる。
セキュリティ
HTTPS
URIのパス、クエリ文字列、リクエスト・レスポンスのヘッダとボディを暗号化する。
HTTPSは、暗号化ライブラリのバグ、クライアントの証明書の検証が行われていない等の理由によって100%安全であるとは言い切れない。
HTTPよりハンドシェイクに時間がかかるため、HTTPS化しなくて良いAPIはHTTPにするなどの工夫もあり。
XXS(クロスサイトスクリプティング)
ユーザーの入力値を元にサイト表示する際に、入力値にスクリプトを埋め込むことでサイト表示時にスクリプトを実行する攻撃。
対策
-
Content-Type
に適切な値を設定する
IEではContent Sniffering
という機能により意味がなくなってしまう。 - レスポンスヘッダに
X-Content-Type-Option: nosniff
を付与
IE7以前では未対応。 - 追加のリクエストヘッダの有無をサーバーで確認する
一般的なのは、X-Requested-With: XMLHttpRequest
- JSON文字のエスケープ
16進数のエスケープを利用する。
エスケープ候補:/,<,>,",',,U+2028,U+2029,UTF-7の+(\u002B)
XSRF(クロスサイトリクエストフォージェリ)
悪意のあるページのリンクを踏むことで別サイトへのリクエストが行われ意図しない操作が実行される。
対策
XSRFトークンを使用する。
あらかじめ付与したXSRFトークンの有無を確認し、ない場合はアクセスを拒否する。
X-Requested-With
の有無でアクセスの可否を判断するなど。
JSONハイジャック
script要素でJSONをvbscriptとして読み込ませることでエラーを発生させ、エラーハンドラ経由でJSONデータを取得する攻撃(難しい)。
対策
- JSONをscript要素では読み込めないようにする
X-Requested-With
がある時のみアクセス可能にする。 - JSONをブラウザがJSONと認識するようにする
正しいメディアタイプを設定する。いつでもこれは大事。
レスポンスヘッダにX-Content-Type-Option: nosniff
を付与。 - JSONをJavascriptとして解釈させない、あるいは実行時にデータを読み込めないようにする
配列ではなくオブジェクトを返却するようにする。
JSONの先頭に無限ループを仕込んで処理を進まないようにする。サーバー側ではループ処理を文字列操作で削除して読み込む。
ここからは悪意のあるユーザーによる攻撃
パラメータの改ざん
クエリーパラメータの値を故意に書き換えて送信する攻撃。
対策
ポイントの不正利用やゲームの勝敗の不正な決定ができてしまうことも。
バックエンド側で値の整合性の検証ができるようにしておく。
マイナス値のチェックも行うなど。
リクエストの再送信
一度送信したリクエストを再送信すること。
クーポンの重複取得、動画再生数の水増し、ゲームの勝利判定の水増しなどができてしまう。
対策
ユースケースに応じた検証方法を設ける必要がある。
動画再生数の場合は短期間でのアクセスは2回目以降カウントしないなど。
モバイル端末での課金アイテム購入では決済に関するレシートのようなデータを利用し、リクエスト時に既に課金で使用されたものか確認することで検証できる。
セキュリティ関係のHTTPヘッダ
X-Content-Type-Options
何度か出てきてお馴染みのヘッダ。
IEのContent Sniffing(メディアタイプが指定されていても、無視してコンテンツの内容や拡張子からデータ形式を判定する機能)を無効にするためのヘッダ。
X-Content-Type-Options: nosniff
JSONをJSON以外として解釈させないためにつける。
APIでJSONを配信する場合は付けておくべき。
X-XSS-Protection
ブラウザのXSSの検出、防御機能を有効にするヘッダ。
IEにはこの機能を無効にする設定があるため、このヘッダで有効にできる。
X-XSS-Protection: 1; mode=block
全てのXSSに対応しているわけではないので注意。
X-Frame-Options
透明なIFRAME
要素をクリックした際に別のサイトで意図しない操作が行われるようなクリックジャッキングの対策ができる。
X-Frame-Options: deny
指定したページがFRAME
,IFRAME
要素ないで読み込まれるかどうかを制御する。
Content-Security-Policy
読み込んだHTML内のLINK要素などの読み込み先にどこと許可するのか指定する。
XSSの危険性を低減できる。
Content-Security-Policy: default-src 'none'
Strict-Transport-Security
ブラウザからのアクセスをHTTPSのみに限定させる。
Strict-Transport-Security: max-age=15768000
HTTPでの接続時にHTTPSへリダイレクトする方法では、初回のHTTPアクセス時に中間攻撃できるため危険。
ただし、このヘッダがあってもHTTPSのアクセス前にHTTP接続してしまうと意味がない。
Public-Key-Pins
HTTP-based public key pinning(HPKP)のためのヘッダ。
SSL証明書が偽造されていないかの検証に使用する。
Public-Key-Pins: max-age=2592000;
pin-sha256="ハッシュ値";
pin-sha256="ハッシュ値"
Set-Cookie
属性によってセキュリティ対策を行う。
- Secure属性
HTTPS通信でのみ、サーバにクッキーを送り返す。 - HttpOnly属性
クッキーがHTTPの通信のみで使用され、スクリプトではアクセスできない。
大量アクセス対策
ユーザーごとにアクセス数を制限する
- リミットレートの単位
全てのエンドポイントに対してか、個々のエンドポイントに対してか - リミットをいくつにするか
- ユーザーの識別に何を使うか
- リミットのリセットタイミング
- リミットを超えた場合の対応
ステータスコード:429 - ユーザーへのリミットの伝え方
レスポンスヘッダでレートリミットを渡す。
終わりに
セキュリティに関しては、別途深める必要があるなと。
付録にWebAPIチェックリストがついているので便利。