APIレート制限は、正しい認証のまま会員情報を大量に引かれる事故を、手前で止めるための仕組みです。2026年10月15日、ソースネクストは「会員情報取得 API」が不正に使われ、最大22,519,960件の会員情報が流出した可能性があると発表しました。

連載「事故から逆算するWebセキュリティ」の第9回は、この事案を入口に、会員情報を返すAPIの量の守り方をまとめます。1回ずつの認可が正しくても、回数が積み上がれば全件に届きます。回数の上限をどこに、何ごとに置くか。上限の内側でゆっくり抜かれたら、どう見つけるか。手元で実行して確かめます。

TWO QUESTIONS認可と量の制御

1回ずつ正しく認可されても、数が積み上がれば抜かれる

1回ずつ見る(認可)

このリクエストは許されるか

  • 正しいトークンか
  • その会員を読んでよいか
  • 1万回目も同じ答えでOKを返す

量で見る(制限と検知)

この鍵はどれだけ使ったか

  • 鍵ごとの回数(レート制限)
  • 重い処理・業務の流れごとの上限
  • 読んだ会員の種類数で検知
出典: OWASP API Security Top 10 2023(API1・API4・API6)をもとに当サイト作成

30秒でわかる(2026年10月17日時点)

  • ・問題:1回ごとの認可は「このリクエストは許されるか」しか見ない。数が積み上がることは止めない
  • ・設計:鍵はIPだけでなくアカウント・トークン・テナント。重い処理と、パスワード再設定や書き出しのような業務の流れには別の上限
  • ・返し方:429とRetry-After。RateLimitヘッダーはまだIETFの草案(-11)
  • ・実測:一気に100回は77回が429。毎秒4回の列挙300回は全部通り、読んだ会員の種類数(300)で見つかった

発表のタイムラインは次のとおりです(ソースネクスト情報漏洩|最大2,252万件・パスワードも対象、会員とAPIを持つ側が確認すること の整理から)。

日時(日本時間) 出来事
2026-10-09 10時頃 会員情報取得 API を使った不正な情報取得が始まる
10-13 9時頃 不正アクセスを確認し、外部からの通信を遮断
10-13 13:36 封じ込めを完了
10-15 最大22,519,960件の流出の可能性を発表。パスワードも対象

悪用された API の脆弱性の中身は公表されていません。本記事はソースネクストで何が起きたかを推測せず、「1件ずつは正しい形のリクエストが、数で積み上がる」型を自分の API で塞ぐ方法に絞ります。事案の読み方の全体像は サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説 にまとめています。

同じ型は、原因まで公表された事案にもあります。アフラックは7月31日の調査結果で、攻撃のアクセスが通常の利用と同じ形式だったため即座に検知できず、短時間に大量のデータ照会があったときの監視・制御の機能も足りなかったと説明しました(漏えいは約440万人)。再発防止策には、データ照会時の認可の強化と、大量アクセスの監視・制御の強化が並びます。10月13日に NICT が注意喚起した「A2B攻撃」も、公開アプリを解析して裏の API を直接叩く手口で、暫定の緩和策に不要な API の停止とアクセス制限、監視の強化を挙げています。

1件ずつ正しくても抜かれる:APIレート制限が要る3つの型

会員情報を返す API が大量に引かれる型は、主に3つあります。どれも、1回ずつのリクエストは認証を通ります。

型 何が起きるか 一次情報での名前
列挙 会員IDを1つずつ変えて読む。他人のIDで返るなら認可の欠陥、返らなくても存在の有無が分かる OWASP API1:2023 BOLA
収集 読んでよい範囲を、正規のアカウントで全部読む OWASP OAT-011 Scraping
リスト型攻撃のあと 他所から漏れたパスワードでログインを試し、入れたアカウントで読む OWASP OAT-008 Credential Stuffing

OWASP の API1 は、IDを受け取るすべての API で「ログイン中の利用者がその対象を操作してよいか」を確かめるよう求めています。これは1回ごとの問いです。1万回目の正しいリクエストにも、認可は同じ答えでOKを返します。回数が積み上がることは、認可の外側で止めるしかありません。

OWASP API4(Unrestricted Resource Consumption)は、一定時間に API を使える回数の制限(レート制限)と、1回の応答で返す件数の上限を挙げています。API6(Unrestricted Access to Sensitive Business Flows)は、購入や予約のように使いすぎると事業を損なう業務の流れを洗い出して守るよう求めています。

APIレート制限の鍵を決める:誰ごと・重さごと・業務の流れごと

レート制限は「何ごとに数えるか」(鍵)で効き方が決まります。IPアドレスだけでは足りません。API6 は、攻撃者が異なるIPアドレスと場所に分散して送る例を挙げています。逆に、会社や携帯回線では多くの人が1つのIPを共有します。

軸 鍵の例 守るもの
誰ごと IP、アカウント、トークン、APIキー、テナント 1つの身元が使える量
重さごと 応答の件数、バイト数、処理時間、外部への送信(SMS など) サーバーとお金
業務の流れごと ログイン、パスワード再設定、会員登録、書き出し 事業そのもの

重さで数える例として、API4 は、パスワード再設定のSMSを1通0.05ドルで送るサービスに、攻撃者が最初の API を数万回呼んで数分で数千ドルを失わせる例を挙げています。API2 は、パスワード再設定の窓口をログインと同じ強さで守り、認証の窓口には通常のレート制限より厳しい上限を置くよう求めています。会員登録の大量作成は OAT-019 Account Creation です。書き出しのように1回で多くの会員を返す API は、回数ではなく返した件数で数えます。

鍵が欠けたリクエストは、数えられずに素通りします。nginx の limit_req は鍵が空のリクエストを数えません。AWS WAF のレートベースルールも、集計キーに指定した要素が無いリクエストを評価から外します。トークンごとの上限を置くときは、トークンの無いリクエストのためにIPごとの上限も残します。

429とRetry-After、RateLimitヘッダーの返し方

上限を超えたら、429 Too Many Requests を返します。RFC 6585 は、429 の応答に事情の説明を含めるべきで、Retry-After で待つ時間を示してよいとしています。誰をどう数えるかは定めていません。429 の応答はキャッシュに保存してはいけません。Retry-After の値は、秒数の整数か日時です(RFC 9110)。

既定値に注意します。nginx の limit_req は、拒否したときの既定のステータスが 503 で、limit_req_status で 429 に変えます。Cloudflare のレート制限ルールは、ブロックの既定が 429 です。

残りの回数を伝える RateLimit ヘッダーは、IETF の HTTPAPI ワーキンググループの Internet-Draft です。リポジトリのタグで最新は -11(2026-04-18 のコミット)で、原稿の中の RFC 番号は「RFC nnnn」のままです。初期の版(-03 など)は RateLimit-Limit・RateLimit-Remaining・RateLimit-Reset の3つでしたが、いまの版は RateLimit-Policy(上限の方針)と RateLimit(いまの残り)の2つです。

草案は、信頼できない相手に処理能力を明かさないよう書いています。401 や 403 も回数に数えると、他人の利用状況を探られうるとも書いています。会員情報の API では、残りを細かく返すことが攻撃者の速度調整を助けます。返すのは自社のアプリや契約先の API に限る判断もあります。

APIレート制限の方式と置き場所

数え方は主に4つです。

方式 数え方 強い点 弱い点
固定窓 毎分0秒からの1分などで回数を数える 作りが簡単。持つ値が少ない 窓の境目をまたぐと、計算上は短い間に上限の2倍まで通る
スライディング窓 直近N秒を振り返って数える 境目の偏りがない 時刻を全部持つ形は記憶が増える
トークンバケット 一定の速さで札を足し、上限まで貯める。1回で1枚使う 短い連続を上限まで許し、平均を抑える 速さと上限の2つを決める
リーキーバケット 一定の速さで処理し、あふれた分を待たせるか拒否する 出ていく速さが一定 待たせる分だけ応答が遅れる

nginx の limit_req はリーキーバケットで、burst の既定は0です。AWS WAF のレートベースルールは、直近60・120・300・600秒のどれかを振り返り、約10秒ごとに数を確かめます。

WHERE TO LIMITどこで止めるか

手前ほど安く落とせ、奥ほど誰が・何を読んだかが分かる

  1. エッジ・WAF

    IP・ヘッダー・JA4・パスで数える。量の多い送信を手前で落とす

    苦手: 拠点ごとの数え方、ログイン後の利用者

  2. APIゲートウェイ

    APIキー・トークンごとに上限。429とRetry-Afterを返す

    苦手: どの会員を読んだか

  3. アプリ

    アカウント・テナント・業務の流れごと。読んだ会員の種類数を数える

    苦手: 量が多いと処理が重くなる

置き場所は1つに決めず、重ねます。エッジは量の多い送信を安く落とせますが、Cloudflare のレート制限ルールは、数を世界全体で共有せずデータセンターごとに持ちます。リクエストを見てから数を更新するまで最大数秒の遅れがあり、決まった数だけを正確に通す作りではないとも書いています。ボットの見分け方は 「人間かボットか」を超えて:CloudflareのPrivacy Pass・Web Bot Auth・Agent Registryで変わるアクセス制御 で扱っています。

「どの会員を何件読んだか」を知っているのはアプリだけです。業務の流れごとの上限と、次に試す種類数の検知はアプリに置きます。

手元で確かめる:トークンバケットと読んだ会員の種類数

Python 3.13.16 の標準ライブラリだけで、会員情報を返す小さな API(/api/members/<ID>)を手元に立てました。トークンからアカウントを引き、アカウントを鍵にしたトークンバケット(上限20、毎秒5回の補充)で数えます。次がその部分です。

class TokenBucket:
    def __init__(self, rate, burst):
        self.rate, self.burst = rate, burst      # 1秒に足す数, 貯められる上限
        self.state, self.lock = {}, threading.Lock()
    def take(self, key):
        now = time.monotonic()
        with self.lock:
            tokens, last = self.state.get(key, (self.burst, now))
            tokens = min(self.burst, tokens + (now - last) * self.rate)
            if tokens >= 1:
                self.state[key] = (tokens - 1, now)
                return True, 0
            self.state[key] = (tokens, now)
            return False, math.ceil((1 - tokens) / self.rate)  # Retry-After は整数秒

LIMIT = TokenBucket(rate=5, burst=20)

# do_GET の中(抜粋)。account は認証済みのトークンから引いたアカウント
ok, wait = LIMIT.take(account)          # 鍵はIPでなくアカウント
if not ok:
    return self.reply(429, {"error": "too many requests"}, {"Retry-After": str(wait)})

送る側も標準ライブラリで書き、10並列で一気に送る場合と、決まった速さで送る場合を比べました(2026-10-17、127.0.0.1)。

条件 送り方 200 429
制限なし 10並列で100回(1.23秒) 100 0
制限あり 10並列で100回(1.24秒) 23 77
制限あり・別のアカウント 同上 24 76
制限あり 毎秒4回で40回 40 0
制限あり・列挙 毎秒4回で300回、会員ID 1〜300 300 0
制限あり・普通の利用 同時に毎秒4回で300回、3人の会員 300 0

一気に送る相手には429が返り、Retry-After はすべて1秒でした。毎秒4回の送信は、上限の内側なので1回も止まりません。ゆっくりした列挙は、回数の制限では見つかりません。

そこで、API が残したログ(1行1件のJSON)から、アカウントごとに1時間で読んだ会員の種類数を数えます。

# 使い方: python3 detect.py access.jsonl
import collections, json, sys
seen = collections.defaultdict(set)                 # (アカウント, 時) -> 読んだ会員ID
for line in open(sys.argv[1]):
    r = json.loads(line)
    if r["status"] == 200:
        seen[(r["account"], int(r["ts"] // 3600))].add(r["member"])
for (account, hour), members in sorted(seen.items()):
    print(account, hour, len(members), "FLAG" if len(members) >= 100 else "ok")

列挙した側は300種類で FLAG、同じ300回を送った普通の利用者は3種類で ok でした(陰性対照)。一気に送った表の2・3行目のログでは、各アカウントが読めたのは23・24種類で、どれも ok です。

MEASURED手元の実測

回数の制限は一気に引く相手を止め、ゆっくり列挙は種類数で見つける

77/100

一気に100回 → 429

上限20・毎秒5回補充。制限なしでは0

0回

毎秒4回で300回 → 429

制限の内側なので全部通る

300種類

その300回で読んだ会員

普通の利用者は3種類

出典: 当サイトの実測(2026-10-17、Python 3.13 標準ライブラリ、ローカル)

しきい値の100と1時間の区切りは、この実験のための値です。区切りは固定窓なので、境目をまたぐ列挙は2つに分かれます。本番では、利用者が普段読む種類数を測ってから決めます。

APIレート制限だけで足りない部分の対策:ページング上限・返す項目・ログ

レート制限は、量の上限を決めるだけです。抜かれたときの被害を小さくし、早く気づくために、次も合わせて入れます。

・1回で返す件数の上限:API4 は、返す件数を決める引数をサーバー側で検査するよう求めている。limit=100000 を受け付けない
・返す項目を絞る:API3 は、to_json() のような丸ごとの変換を避け、返す項目を選んで最小にするよう求めている。画面に出さない生年月日や電話番号は返さない
・推測しにくいID:API1 は、連番より推測しにくい値をIDに使うよう挙げている。ただし認可の代わりにはならない
・種類数と存在しないIDの率を見る:1アカウントが読んだ会員の種類数に加え、404 の多さも列挙の兆候になる
・試してから止める:nginx の limit_req_dry_run は、止めずに超えた数だけを記録する

ログには、時刻、アカウントID、トークンの識別子(値そのものは残さない)、送信元IP、エンドポイントの型(/api/members/{id})、読んだ会員ID、ステータス、返した件数、レート制限の判定を残します。nginx なら判定は $limit_req_status に PASSED・REJECTED などで入ります。会員IDが残っていないと、漏れた範囲を後から特定できません。

影響範囲を決めるのは、1つのトークンで読める会員の範囲と、上限に届くまでの時間です。上限を毎秒5回にしても、1日では43万回に届きます。日ごとの上限と種類数の検知を重ねます。

本記事で確かめていないこと

・nginx の limit_req、Cloudflare と AWS WAF のレートベースルールは文書で確かめただけで、動かしていない
・実験は127.0.0.1の小さな API で、複数のサーバーで数を共有する構成(Redis など)は試していない
・RateLimit ヘッダーの草案の IETF での審議状況は、datatracker ではなく作業グループのリポジトリのタグと原稿で確かめた

参照ソース