過去2回の記事でREST API 設計指針をまとめてきました。
REST API 設計指針・認証認可編
https://serverless.co.jp/blog/mi3tt22d0/
REST API 設計指針・通信、パラメーター編
https://serverless.co.jp/blog/6robfbrairng/
今日は第三回かつ最終回のセキュリティ編です。セキュリティは非常に幅広い概念であり、考慮すべきことは山盛りですが、まずは基本的な考え方から。
加害者と被害者の逆転現象
悪意のある第三者からの攻撃などにより何某かのインシデントが発生して、サービスが停止したり、情報漏洩が起きてしまった場合、サービス事業者はステークホルダーにお詫び、時には直接的な金額による賠償を行うことになります。本来システムを攻撃された被害者側ですが、加害者であるかのような扱いをされるケースがあります。一方インシデントの種別によっては世の中が同情的になるケースもあります。この違いについてですが、一般的によく用いられる対策をとっていたかどうかが大きな分岐点となります。
攻撃されたシステム、およびその利用者に対して責任を持つ側からすると攻撃とはこういう風に理解されます。

一方そのシステムであったり実際に発生した攻撃の詳細を把握していないステークホルダーからするとそのインシデントはこう見えています。

時間とともにいろいろな情報が明らかになるにつれ、どちらに非があるのかがなんとなく決まっていくことになります。大型のB2Cサービスであれば特にSNSの影響は大きく、どのような対策を行っていたか?特に一般的に用いられている対策は行われていたのか?が重要なパラメータとなります。
ではここから、APIを保護するための一般的な内容をまとめていきます。これがすべてではなく、必要なセキュリティというものは刻一刻と変わっていくことに注意してください。
定期的なアクセスログの解析と権限の棚卸
認証認可編でふれましたが、クラウドのAPI基盤サービスを活用した場合、アプリケーションレイヤでの認証認可とは別に、呼び出されるAPIそのものがその他同クラウド内の別リソース(データベースやストレージなどへの読み取りや書き込み)の権限を保有します。この権限を定期的に棚卸を行い、利用されていない権限を削除するというのは重要な運用になります。
例えばAWSでは IAM Access Analyzerというサービスがあります。あらかじめこの機能を有効化しておくことで、利用されていない権限の洗い出しを行うことが可能です。
また、全てのログをSIEM(Security Information and Event Management)ツールと
連携させることで不正なログを検知します。昨今はどのツールでも機械学習を用いており、普段と明らかに異なる異常値が発生した場合にアラートを挙げてくれます。
DataDog,NewRelic,Splunkなどが著名なツールです。AWSでいえばSecurity Hubなどがそれにあたります。
レート制限
APIへのリクエスト頻度を制限して、サービスの過負荷を防ぎ、DDoS(分散型サービス拒否攻撃)やブルートフォース攻撃を防ぎます。一般的にはAPIホスティング基盤単独ではなくWAFと組み合わせることでより効果的な防御が可能となります。
・スパイクポリシー:特定時間間隔での呼び出し回数制限
・クォータポリシー:1日の回数などより長い時間での呼び出し回数制限
・APIキーポリシー:認証のAPIキー単位で呼び出し回数制限
・複数条件付きポリシー:特定IPアドレス、特定の国からの呼び出し回数制限等複数条件での組み合わせ
入力検証とサニタイズ
REST APIのフォーマットを定めるOpenAPI3.0という規格があります。(最新版は3.1)これに従いPOSTされた値が正当かどうかを検証可能な機能をAPIホスティング基盤やWAFが提供するケースがあります。AWSはその機能はないようですが代表的なものでいえばGoogle CloudのApigeeなどがあります。例えば変数の数、名前、変数に使われるデータ種別(整数、文字列、など)などをチェックしてくれます。基盤のその機能がない場合、アプリケーションレイヤで設定を行います。
正規表現マッチ
この際正規表現マッチはプログラムの処理上高速に動作しますので利用を推奨します。例えばJavaScriptでは以下で文字列の中に記号が入っていないこと、アルファベットと数字だけで構成されること、などが確認できます。
function hasNoSymbols(str) {
return /^[a-zA-Z0-9]*$/.test(str);
}
console.log(hasNoSymbols("abc123")); // true
console.log(hasNoSymbols("abc@123")); // false
サニタイジング
同様にSQLインジェクション攻撃などを考慮して記号を入力として受け付ける場合サニタイジングを行い、意図しないコードの実行を防ぎます。
var userInput = request.formparam.username;
var sanitizedInput = userInput.replace(/<[^>]*>/g, ''); // HTMLタグを削除
response.headers['X-Sanitized-Input'] = sanitizedInput;
こうすることでHTMLのタグに使われる文字がインプットとしてリクエスタから提出された場合、その値を安全な形にすることができます。多くのウェブサイトではパスワードに[!]が使えないケースがありますが、攻撃者にとっては、裏側のアプリケーションにおいて苦手とする記号、すなわち誤作動が期待される記号として認識されることに注意する必要があります。
このあたりの処理はWAFやAPI基盤によっては標準で提供されていますので、アプリケーションへの実装前にまず利用できるものがないか調査を行うことを推奨します。
エラーハンドリングとメッセージのセキュリティ
エラーメッセージの隠匿化
開発時は便利なのでそのままリリースしてしまいがちですが、画面に表示されるエラーメッセージは攻撃者に推察材料を与えてしまうため、本番リリース時には一般的なメッセージに変えることが重要です。
HTTP 50xエラーメッセージの変更
一般的にHTTP REST通信において500番台エラーメッセージは、内部プログラムエラーを指します。攻撃者に、どうすればプログラムが不正終了するか?を推察させないため、HTTPステータスコード200に500番台エラーを書き換えレスポンスを返すようにします。
ライブラリ管理
昨今のソフトウェア開発においてはOSSベースのライブラリを多く多用しています。OSSはモジュールの開発に別のOSSモジュールを用いていることも多く結果としてソフトウェア構成が複雑化しライセンス形態の管理も同時に複雑になります。
Magecart攻撃やサプライチェーン攻撃への対策
悪意のあるモジュールを中継路で混入させ、知らずに使ったウェブ開発者により作成されたウェブサイトへアクセスしたブラウザが、入力するクレジットカード情報を別の場所へ知らない間にPOSTさせるのがMagecart攻撃です。またクレジットカード以外にも同様に攻撃を行うことをサプライチェーン攻撃といいます。これを防ぐために以下の手段が有効です。
・前の記事で解説したContent Security Header の付与
これにより通信可能なドメインを制御します。
・定期的なマルウェアスキャン
悪意のあるスクリプトをブラウザに読み込ませることを防ぎます
SBOM
上記の攻撃と同様の手口で、異なるライセンスのソフトウェアを混入させるケースもあります。例えば関連するソースコードの開示や知財の放棄を謳っているライブラリが混入していた場合、情報漏洩はしないまでもビジネス的なインパクトが大きくなります。これを防ぐソリューションがSBOMです。
https://www.meti.go.jp/press/2024/08/20240829001/20240829001.html
パッケージマネージャの必須化
例えばJavaScript(node)関連であればnpmというパッケージマネージャを使うことでインストールされているライブラリの一覧や依存関係を常に把握することが可能です。しかしながらnpmを用いたソフトウェアのインストール元が汚染されているケースも過去観測されています。そのような場合、専用プライベートレポジトリを企業セキュリティ専門部隊が整備し、開発部隊はパブリックレポジトリではなく当該レポジトリからのソフトウェアインストールのみに限定させることが可能です。
コストや専門性の観点からそれなりの開発規模でないと難いため、一般的にはアンチウイルス、ペネトレーションテスト等で代替します。
バージョニングの基本的な考え方
セキュリティとは異なりますが、APIをバージョンアップしたい時の考え方です。APIをバージョンアップする際に、リクエスト側の設定変更はサービス利用体験を悪くするため避けたい一方で、バージョンアップ後も古いAPIが利用され続けるのは避けたいものです。
URLによるAPI互換性の維持
バージョンアップ時に仕様が変更になることを想定し、特定クライアントは引き続き古いバージョンのAPIを利用できるようにしなければならないケースがあります。(セキュリティ目的のバージョンアップ時にはお勧めできません)。これを考慮し提供するAPIを以下のように設計します。
/v1/users: バージョン1のユーザーAPI
/v2/users: バージョン2のユーザーAPI
HTTPヘッダーによるAPI互換性の維持
上記の方式は視認性は楽ですが、バージョンアップ時にクライアントもプログラムを変更しなければならないため手間が発生します。その場合リクエストHTTPヘッダーで古いバージョンを指定したいときのみ値を挿入する、という手法があります。以下の例では` vnd.example.v1+`が指定されたときのみv1を呼び出し通常の` application/json`では最新版を呼び出します。
GET /users HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.v1+json
AcceptヘッダーではなくContent-Typeヘッダーでの切り替えも可能です。
POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/vnd.example.v1+json
Accept: application/json