データベースラッパーシステム
この言い方が正しいかわかりませんが、データベースをラッパー(データベースの操作などをしてくれるAPIみたいなもの)するシステムを今回は対象にしています。
例えば、商品の在庫管理システムなどで商品を登録して、条件に合う商品の在庫状況を取得するシステムのようなものです。
この場合以下のようなシステムになります。
- データベース
商品のID、在庫などが保存されている - バックエンド(ラッパーシステム)
APIを叩く(バックエンドにリクエストを送る)と、商品の情報の取得・登録・在庫情報の取得ができる
今回は、メインはセキュリティの方なので、商品を管理するシステムに焦点を当てようと思います。
商品管理システム
API エンドポイントの概要は以下です。
Product
というデータベースを用意し、そのデータベースに対して操作を行う機能を提供します。
-
GET /health
: ヘルスチェック -
POST /api/v1/products
: 商品作成 -
GET /api/v1/products
: 商品一覧 -
GET /api/v1/products/:id
: 商品詳細 -
PUT /api/v1/products/:id
: 商品更新 -
DELETE /api/v1/products/:id
: 商品削除
デモとして、以下のgithubにあげているのでdockerが使える方は、一度試してみてください。
セキュリティ
考えられる攻撃・障害
さて、このシステムで必要な障害対策は何があるでしょうか?
このシステムが脆弱(特に何の障害対策もしていない)であるとき、どのような攻撃ができるかを考えてみましょう。
- まずデータを登録するときや一覧を取得するときのクエリに攻撃文を入れる攻撃
- 単純にリクエストをいっぱい送ってサービスの提供不能にする
また、エラーが起きる可能性については以下のようなことが考えられます。
- データにアクセスするプロセスが複数あり、競合が起きる。
- データへのアクセス制限(ロック)が複数おき、複数のプロセスが互いのロック解除を待っている状況(デッドロック)
- 同じリクエストがリクエストが2回送られてきた時に、違うレスポンスを返してしまう。
- 単純にデータベースに接続ができなくなる
障害対策
とりあえず、このあたりの障害対策を行なっていきましょう!
クエリに攻撃文を入れられることへの対策
クエリに攻撃文を入れる方法の一つとしてSQLインジェクション
があります。
SQLインジェクション
例えば以下のような商品名に一致するデータを取得するSQL文があったときに、
リクエスト
{
product_name = "product_name"
}
SELECT * FROM Product WHERE Name = 'product_name'
product_name
のところにリクエストから送られてきた文字列をそのまま入れていると、以下のように常に真となる条件が勝手に入れられてしまう
リクエスト
{
product_name = "product_name' or 'a'='a"
}
SELECT * FROM Product WHERE Name = 'product_name' or 'a'='a'
このSQLインジェクションを防ぐための手法は以下があります。
- 入力のバリデーションを設定する
- リクエストやデータの型を指定する
-
"
を%32
など文字を変換する(サニタイズ) - バインド機能を使用する
バインド機能
以下のように?
を使い、パラメータを指定することで変数の部分は必ず文字列として認識してくれる機能
SELECT * FROM Product WHERE Name = ?
大量のリクエストへの対策
大量のリクエストに対しては、インフラレベル(ファイアーウォールやロードバランサーなど)の対策もありますが、今回はアプリケーション側の対策を考えます。
ファイアーウォール
ファイアーウォールは、特定のIPアドレスからのアクセスを制限することができる機能です。
ロードバランサー
ロードバランサーは、トラフィックを複数のサーバーに分散させることで、システムの可用性を向上させる機能です。
- キャッシュ機能を有効にする
- 特定のIPアドレスからのアクセスに対するレート制限を行う
以上のようにミドルウェアを設定することも、一つの対策手段となります。
データの競合への対策
データの競合は、データベースのトランザクションを使用することで解決できます。
トランザクション
トランザクションは、データベースに対する一連の操作を一つの単位として扱うことができる機能です。
BEGIN TRANSACTION;
UPDATE Product SET stock = stock - 1 WHERE id = 1;
COMMIT;
トランザクションを使用することで、データの整合性を保つことができます。
ただし、データの更新などではトランザクションのみでは不十分な場合があります。
例えば、データ情報を取得してから、データを更新する場合などです。
この場合、データの取得と更新の間に他のプロセスがデータを変更してしまう可能性があります。
この場合は、データの取得と更新を一つのトランザクションとして扱うか、バージョン管理を行うことで、データの整合性を保つことができます。
バージョン管理
バージョン管理は、データの変更履歴を管理することで、データの整合性を保つことができる機能です。
保存しているデータにバージョン情報を持たせることで、データの変更履歴を管理することができます。
これによって、データの取得時のバージョンと、データの更新時のバージョンを比較することで、異なっていれば、データの更新を行わないようにすることができます。
同じリクエストが2回送られてきた時の対策
フロント側や、APIを叩く側で間違えた場合や、ネットワークエラーの再送処理などにより2回連続でリクエストを送ってしまうことがあります。
この時に、同じレスポンスを返さなければ、クライアント側で混乱を招く可能性があります。
また、データの登録処理の場合、データの重複登録を行なってしまう可能性があります。
この場合は、リクエストに対してのレスポンスをキャッシュすることや、idempotency keyを使用することで、同じリクエストに対して同じレスポンスを返すことができます。
idempotency key
idempotency keyは、リクエストに対して一意のキーを付与することで、同じリクエストに対して同じレスポンスを返すことができる機能です。
ただし、リクエストの時点で一意のキーを付与してもらう必要があります。
これを行うことによって、POSTリクエストに対しても冪等性を持たせることができます。
データベースに接続できなくなることへの対策
これはアプリケーション側ではどうしようもないので、正しくエラーハンドリングを行なって返すことが重要です。
他にも
セキュリティ対策は、他にも様々な対策があり突き詰めればキリがありません。
例えば、以下のようなことが考えられます。
- データベースのバックアップを定期的に行う
- データベースの暗号化を行う
- データベースのアクセス制限を行う
- データベースの監視を行う
- データベースのログを監視する
- データベースのパフォーマンスを監視する