はじめに
Lambdaでは、パフォーマンスを向上させるために、一度起動した実行環境を一定期間破棄せずに保持し、
後続のリクエストで再利用する仕組みがあります。
この仕組みにおいて、ハンドラー関数の外側で初期化された変数やクラスのインスタンスは、
リクエスト処理が完了してもメモリ上から消去されず、次のリクエスト処理時にそのまま共有されます。
通常、この仕様はデータベース接続の使い回しなどに利用されます。
しかし、「特定のリクエストに紐づくデータ(ユーザーの検索結果や許可リストなど)」をグローバル変数として保持しまうと、
前のユーザーのデータが次のユーザーのリクエストに混入するという重大なバグを引き起こします。
他人の個人情報が見えてしまう「情報漏洩」にもつながります。
下記のDemoコードを一緒に分析しましょう。
Demoコード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| Database = {
'User': {
'user_1': {'equipment_1'},
'user_2': {'equipment_2'},
},
'Equipment': {
'equipment_1': {'name': 'equipment_info_1'},
'equipment_2': {'name': 'equipment_info_2'},
}
}
class DataGetter:
def __init__(self):
# 【Bugの原因】
# __init__ はLambdaのコンテナ起動時(コールドスタート)に一度だけ実行される
# そのため、self.allowed_equipment_id はリクエスト間で共有される
self.allowed_equipment_id = set()
def execute(self, body):
user_id = body['userId']
# リクエストした機器にユーザー権限があるかどうか
for equipment_id in body['requestEquipmentId']:
if equipment_id in Database['User'][user_id]:
self.allowed_equipment_id.add(equipment_id)
# 本来はそのリクエストで許可されたものだけを返すべきだが、
# 前回のリクエストで追加されたIDも残っているため、
# 本来権限がないはずの機器情報まで返却してしまう
return [Database['Equipment'][equipment_id] for equipment_id in self.allowed_equipment_id]
# ハンドラーの外でクラスをインスタンス化している(グローバル変数)
# これにより、AWS Lambdaのコンテナが再利用される限り、
# 同じ data_getter オブジェクトが使い回される
data_getter = DataGetter()
def lambda_handler(event, context):
response = data_getter.execute(event['body'])
return response
|
発生現象
一回目のリクエストは、【機器1】のみの権限を持つ【ユーザー1】が実行する
1
2
3
4
5
6
| {
"body": {
"userId": "user_1",
"requestEquipmentId": ["equipment_1", "equipment_2"]
}
}
|
下記のレスポンスが返却される。これは正しい結果です。
1
2
3
| [
{"name": "equipment_info_1"}
]
|
この実行によって、メモリ上のインスタンス内には {'equipment_1'} が残ったままとなります。
二回目のリクエストは、【機器2】のみの権限を持つ【ユーザー2】が実行する
1
2
3
4
5
6
| {
"body": {
"userId": "user_1",
"requestEquipmentId": ["equipment_2"]
}
}
|
下記のレスポンスが返却される。【機器1】がリクエストされていないにも関わらず、レスポンスの中に返却されてしまいました。
【ユーザー2】は本来閲覧権限のない【機器1】の情報を取得できてしまい、越権アクセス・情報漏洩が発生しました。
1
2
3
4
| [
{"name": "equipment_info_1"},
{"name": "equipment_info_2"}
]
|
修正方法
この問題を解決する最も確実な方法は、状態を持つオブジェクトの初期化をハンドラー関数の内部(ローカルスコープ)で行うことです。
1
2
3
4
5
6
7
8
9
10
11
12
| # 【修正ポイント】
# グローバル領域でのインスタンス化を削除する。
# data_getter = DataGetter() <-- 削除
def lambda_handler(event, context):
# 【修正ポイント】
# ハンドラー内部でインスタンス化を行う
# これにより、リクエストごとに新しい「空の」data_getterが生成される
local_data_getter = DataGetter()
response = local_data_getter.execute(event['body'])
return response
|