ばとらの部屋

不本意ながらDoS攻撃をしてしまった失敗談

作成日 2025年2月9日 / 最終更新日 2025年2月9日

はじめに

この記事は特にWebプログラミング始めたての初心者向けで、私を反面教師としてください

Web上級者の方はコイツバカだなぁと笑って見過ごしてください...(泣

私が所属する大学のサークルでは、AtCoderといういわゆる競技プログラミングのコンテストに参加することを活動の一つとしています。

そこで、サークルメンバー(以下部員とする)がより一層切磋琢磨してレートを上げてモチベーションに繋げてほしいな~と思い、そうだ!毎回のコンテストの結果から順位表を作ってみんなに見れる形にしよう!ということでAtCoder Leaderboardを作ろうと計画しました。

しかし、最終的には残念な結果になります

AtCoderのAPIを取得したい

AtCoderでは公式ではないものの、コンテストページやプロフィールページでURLの末尾に/jsonと追加すると、該当ページのデータをjson形式のファイルで取得することができます。

何も考えずに、これいいじゃん!と思ってここからAPIを取得する意向を決めました。

実装

まずは簡単にシーケンス図で説明をします。今回、APIをFetchする際にCORSの問題が生じてしまい面倒なので自分でプロキシサーバーを立てて仲介するような感じで行います。

部員のユーザー情報を取得する流れ

今回はHonoというWebフレームワークを使用し、Cloudflare Workersへデプロイをします。

これを踏まえて、コーディングをします。

app.post("/api/atcoder/users", async (c) => {
  const users = await c.req.json();
  if (!users) return c.text("Bad Request", 400);
  if (!Array.isArray(users)) return c.text("Bad Request", 400);
  if (!users.every((user) => typeof user === "string"))
    return c.text("Bad Request", 400);
  const userData = [];
  for (const user of users) {
    const cache = await c.env.USER_CACHE.get(user);
    if (cache) {
      const { data, timestamp } = JSON.parse(cache);
      if (Date.now() - timestamp < 1000 * 60 * 60) {
        userData.push(data);
        continue;
      }
    }
    const res = await fetch(`https://atcoder.jp/users/${user}/history/json`);
    const data = await res.json();
    userData.push(data);
    await c.env.USER_CACHE.put(user, JSON.stringify({
      data,
      timestamp: Date.now(),
    }));
    await new Promise((resolve) => setTimeout(resolve, 300));
  }
  return c.json(userData);
});

やっていることとしては、/api/atcoder/usersというエンドポイントを作成し、フロントエンドから送られてきた部員のリストを1件ずつatcoder.jpへfetchします。

アクセス毎に数十人のリクエストを送っているとフロントエンドでの表示に時間がかかってしまうため、キャッシュを行います。

Cloudflare WorkersにはKVというアプリケーションデータをCloudflare上に保存することができるサービスがあるのでそれを使います。

無事プロジェクトを作り終え、サイトを公開しました。

事件発生

ある日、サイトを見に行くと順位表が表示されませんでした。なにか仕様が変わったのかなと思いコードを見直したり、Postmanを使って通信の内容を確認しました。

エラーを見た感じどうやらAPIの取得がうまくいっていない...

しかし、ローカルで動かすとちゃんと取得ができている...何故だ?

プロキシサーバーからアクセスするとこうなる

KVのログも見てみると同じようなメッセージ

よく読んでみると

Request blocked. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.

簡単にいうと、アクセスが多すぎてお前のリクエストはブロックされたよって言われてます。

AtCoderのサイトにDos攻撃をしてしまいIPBANされてブラックリスト入りということになります。猛省。

なにがいけなかったか

私はDoS/DDoS攻撃が知らないエンジニアというわけではありません。あくまで知識としては知ってました。

まあ実際、アクセス拒否食らってるから知ったかぶりをしてたようなもんですが...

ソースコードの問題

ソースコードを見て気づいた方もいると思いますが問題はここです。

await new Promise((resolve) => setTimeout(resolve, 300));

はい。今思えばこれは良くない。

リクエストの間隔が300msつまり0.3秒に1回のリクエストが送られることになります。

テストの段階で2、3人のデータで行っている分には問題ないと思いますが、実際には30人近くの部員を0.3秒間隔でリクエストを送っていました。普通に考えてリロードボタンを1秒に3回押しているようなものです!

ここで少し言い訳タイム。 DoS攻撃ってもっと1秒間に何十回もリクエストを送るものだと思っていたため、こんなもんじゃDoS攻撃にならないでしょと油断していた自分がいました。

キャッシュの問題

キャッシュにも問題があります。

if (Date.now() - timestamp < 1000 * 60 * 60) {
  userData.push(data);
  continue;
}

AtCoderのコンテストは毎週土曜日に行われるので少なくとも週に1回の更新で済みます。ですが、キャッシュの有効期限を1時間に設定してしまうのもよくありません。

またキャッシュのやりかたも改善できます。

例えば

  • アクセスがない時も毎分1人のデータを取得してキャッシュする
  • リクエスト間隔を長くし取得できたユーザーから順にフロントエンドで表示させる

などなど?つまりは一度に大量のアクセスをしないようにするということです。(当然)

APIの仕様の問題

APIの仕様の問題と書きましたが、これは単に非公式のAPIを使ってしまったということです。

事実上AtCoder様からブロックされてしまいましたが、もしこれが公式のAPIで利用規約をちゃんと読みAPIの取得は何件までですよ~ とか記載されていたらちゃんと読んで従うようにしましょう。

また、AtCoderに限った話だとAtCoder Problemsというサイトがあり、ここではAPIの仕様書も記載されています。なのでもしAtCoderのAPIを使いたい場合はこちらを使うことをお勧めします。

さいごに

APIは非常に便利ですが、個人でWeb開発を行う際はAPIの使い方に十分気を付けてください。