Python WebAcademy Blog

Tornado 6.6がリリース。設定1行でなりすましの送信を防ぐcross_origin_protectionと、危ないリダイレクトの修正を初心者向けに解説

|

2026年10月21日、PythonのWebフレームワークTornadoの6.6.0が公開されました。別のサイトから勝手に送られてくるリクエストを、設定1行で防げるcross_origin_protectionが加わり、利用者を外部のサイトへ飛ばせてしまうリダイレクトの問題も直っています。6.5.10と6.6で同じアプリを動かした結果とともに、上げる前に確かめたい点を整理します。

2026年10月21日、PythonのWebフレームワークTornadoの6.6.0が公開されました。今回の目玉は、cross_origin_protectionという新しい安全機能です。

これは、別のサイトから勝手に送られてくるリクエストを、設定を1行加えるだけで防げる仕組みです。ほかにも、利用者を外部のサイトへ飛ばせてしまうリダイレクトの問題などが直され、Python 3.11以上が必要になりました。

Tornadoを使っていない方にも、Webアプリの安全を考えるうえで知っておきたい話が詰まっています。今回は、6.5.10と6.6で同じアプリを動かしながら、何が変わったのかを見ていきます。

まずはTornadoとはどんなものか

最初に、Tornadoを知らない方のために、どんなフレームワークなのかを簡単に紹介します。Tornadoは、たくさんの接続を同時にさばくのが得意な、歴史のあるWebフレームワークです。

Pythonの非同期処理の仕組みであるasyncioの上で動き、長くつなぎっぱなしにする通信と相性がよいのが特徴です。ブラウザとサーバーが双方向にやりとりするWebSocketを使うアプリなどで、よく使われています。

非同期処理そのものについては、こちらで紹介しています。【関連記事】Pythonのasyncioとは?

ほかのフレームワークとの位置づけ

PythonのWebフレームワークはたくさんあります。代表的なものと比べると、次のような位置づけです。

フレームワーク 特徴
Django 管理画面やログインなど、必要なものが一通りそろっている
Flask 小さく始めて、必要なものを足していける
FastAPI 型ヒントを使ってAPIを手早く作れる
Tornado 非同期処理で、多くの接続を同時にさばくのが得意

小さなWebアプリの作り方から知りたい方は、Flaskの記事も参考になります。【関連記事】PythonのFlaskとは?

6.6で何が変わったのかを整理しよう

続いて、6.6.0の主な変更を表にまとめます。内容は、Tornadoの公式リポジトリにある6.6.0のリリースノートにもとづいています。

分類 主な変更
新機能 なりすましの送信を防ぐcross_origin_protectionを追加
安全面 リダイレクトで外部のサイトへ飛ばされる問題などを修正
動きの変更 キューやロックでtimeoutに0を渡すと、すぐに結果を返すように
Pythonの対応範囲 3.15に対応し、3.9と3.10のサポートを終了
対応環境 フリースレッド版のPythonやWindows on ARMに対応

PyPIの記録によると、6.6が必要とするPythonは3.11以上です。ひとつ前の6.5.10は、2026年9月15日に公開されていました。

主要なライブラリがPython 3.10を外していく流れは、先日の記事でもまとめました。【関連記事】Python 3.10のサポート終了から2週間、主要ライブラリの対応状況

なりすましの送信とは何か

新機能の話に入る前に、それが防ぐ攻撃について知っておきましょう。英語ではcross-site request forgeryと呼ばれ、略してCSRFやXSRFと書かれます。

日本語では、クロスサイトリクエストフォージェリと呼ばれることが多い攻撃です。forgeryは偽造という意味で、利用者になりすましてリクエストを送らせる攻撃のことです。

どんな流れで起きるのか

たとえば、ネット銀行にログインしたまま、別の怪しいサイトを開いたとします。そのサイトに、ネット銀行へ送金のリクエストを送る仕掛けが隠されていたらどうでしょうか。

ブラウザは、ネット銀行へのリクエストに、ログイン中であることを示すクッキーを自動で付けてしまいます。その結果、利用者が何も操作していないのに送金が実行される、という事態が起こりえます。

設定1行で使えるcross_origin_protection

Tornado 6.6では、この攻撃を防ぐ仕組みが加わりました。公式ドキュメントによると、アプリの設定でcross_origin_protectionをTrueにするだけで有効になります。

どうやって見分けているのか

今のブラウザは、リクエストを送るときに、Sec-Fetch-Siteというヘッダーを付けています。これは、そのリクエストが同じサイトから来たのか、別のサイトから来たのかを示す情報です。

公式ドキュメントによると、この設定を有効にすると、POSTやPUTのように状態を変えるリクエストで、Sec-Fetch-Siteがsame-originやnone以外なら拒否します。このヘッダーがない場合は、OriginとHostという2つのヘッダーが一致するかで判断します。

6.5.10と6.6で動かしてみた

実際に試してみましょう。送金を受け付けるという想定の小さなアプリを作り、Sec-Fetch-Siteの値を変えてPOSTを送ります。

import asyncio

import tornado
from tornado.httpclient import AsyncHTTPClient
from tornado.web import Application, RequestHandler


class TransferHandler(RequestHandler):
    def post(self):
        self.write("送金しました")


async def main():
    print("Tornado", tornado.version)
    app = Application(
        [(r"/transfer", TransferHandler)],
        cross_origin_protection=True,
    )
    app.listen(8888)
    client = AsyncHTTPClient()

    cases = {
        "同じサイトから": "same-origin",
        "別のサイトから": "cross-site",
    }
    for label, site in cases.items():
        res = await client.fetch(
            "http://localhost:8888/transfer",
            method="POST",
            body="",
            headers={"Sec-Fetch-Site": site},
            raise_error=False,
        )
        print(label, res.code)


asyncio.run(main())

6.5.10と6.6を、それぞれ新しい仮想環境に入れて、手元のPython 3.11で実行した結果です。動作に関係のないログの行は省いています。

Tornado 6.5.10
同じサイトから 200
別のサイトから 200

Tornado 6.6
同じサイトから 200
別のサイトから 403

6.5.10では、この設定はまだ存在しないので、別のサイトからのリクエストも受け付けてしまいました。6.6では、別のサイトからのリクエストが、403という拒否のコードで止められています。

これまでのxsrf_cookiesという仕組みでは、HTMLのフォームに確認用の値を埋め込む必要がありました。公式ドキュメントによると、新しい仕組みはHTMLやJavaScriptを書き換えなくてよいので、ずっと手軽に使えます。

今後の予定と注意点

公式ドキュメントによると、6.6ではこの設定の既定値はFalseで、6.7で既定で有効になる見込みです。xsrf_cookiesは6.7で非推奨になる見込みですが、7.0より前に取り除かれることはないとされています。

別のサイトからの送信を許したい場合は、trusted_originsという設定に許可するオリジンを並べます。オリジンとは、https://example.comのように、方式とドメインとポートを組み合わせたものです。

なお、この機能は、CORSと呼ばれる別のサイトからデータを読ませる仕組みとは無関係だと、公式ドキュメントに明記されています。名前が似ているので、混同しないようにしましょう。

危ないリダイレクトが直った

安全面では、リダイレクトの修正も重要です。リダイレクトとは、あるURLにアクセスした人を、別のURLへ自動で案内する仕組みのことです。

Tornadoには、URLの一部を使って案内先を決めるRedirectHandlerがあります。リリースノートによると、その値を細工すると、案内先を外部のサイトに変えられてしまう問題がありました。

実際に試してみた

/go/helpにアクセスすると/helpへ案内する、という設定で、2つの版を比べてみました。2つ目のアクセスでは、わざと/go//evil.exampleという細工したURLを使っています。

import asyncio

import tornado
from tornado.httpclient import AsyncHTTPClient
from tornado.web import Application, RedirectHandler


async def main():
    print("Tornado", tornado.version)
    app = Application([(r"/go/(.*)", RedirectHandler, {"url": "/{0}"})])
    app.listen(8889)
    client = AsyncHTTPClient()
    for path in ["/go/help", "/go//evil.example"]:
        res = await client.fetch(
            "http://localhost:8889" + path,
            follow_redirects=False,
            raise_error=False,
        )
        print(path, res.code, res.headers.get("Location"))


asyncio.run(main())

実行した結果です。Locationは、案内先のURLを表すヘッダーです。

Tornado 6.5.10
/go/help 301 /help
/go//evil.example 301 //evil.example

Tornado 6.6
/go/help 301 /help
/go//evil.example 400 None

6.5.10では、案内先が//evil.exampleになりました。スラッシュ2つで始まるURLは、ブラウザでは別のドメインへの移動として扱われるので、利用者が外部のサイトへ飛ばされてしまいます。

6.6では、こうしたリクエストが400のエラーで止められました。この手口はオープンリダイレクトと呼ばれ、偽のログイン画面へ誘導する詐欺などに悪用されることがあります。

timeoutに0を渡したときの動きが変わった

最後に、静かに動きが変わる変更も紹介します。リリースノートによると、キューやロックの待ち時間を表すtimeoutに0を渡すと、すぐに結果を返すか、エラーを出すようになりました。

これまでは、0が、無期限に待つことを表すNoneと同じに扱われていたそうです。空のキューから取り出すQueue.get()で試してみました。

import asyncio
import time

import tornado
from tornado.queues import Queue


async def try_wait(label, coro):
    start = time.perf_counter()
    try:
        # 念のため、2秒で打ち切る
        await asyncio.wait_for(coro, 2)
    except TimeoutError:
        pass
    print(f"{label}: {time.perf_counter() - start:.1f}秒")


async def main():
    print("Tornado", tornado.version)
    await try_wait("Queue.get", Queue().get(timeout=0))


asyncio.run(main())

実行した結果です。

Tornado 6.5.10
Queue.get: 2.0秒
Tornado 6.6
Queue.get: 0.0秒

6.5.10では、2秒で打ち切るまで待ち続けていました。6.6では、待たずにすぐTimeoutErrorになっています。

timeoutに0を渡して、無期限に待つつもりで書いていたコードは、6.6では動きが変わります。上げる前に、検索して確かめておきましょう。

上げる前にやっておきたいこと

ここまでの内容を踏まえて、Tornadoを6.6へ上げる前に確かめておきたいことを整理します。

順番 確認すること
1 Python 3.11以上を使っているか
2 cross_origin_protectionを有効にして、自分のサイトからの送信が通るか
3 別ドメインから送信を受けるなら、trusted_originsに登録したか
4 キューやロックで、timeoutに0を渡していないか
5 RedirectHandlerで、URLの置き換えを使っていないか

私は10年ほどエンジニアとして開発に関わってきましたが、なりすましの送信への対策は、フォームを1つ追加するたびに確認用の値を埋め忘れていないかを気にする、地味で漏れやすい作業でした。設定1行でまとめて守れるのは、現場にとって本当にありがたい変更です。

以前、ログイン後に元のページへ戻すためのリダイレクトで、戻り先のURLを確かめていなかったために、セキュリティ診断で指摘を受けたことがあります。利用者から受け取った値で案内先を決めるときは、必ず自分のサイトの中かを確かめるのが基本です。

まとめ

2026年10月21日、Tornado 6.6.0が公開されました。設定1行でなりすましの送信を防げるcross_origin_protectionが加わり、外部のサイトへ飛ばせてしまうリダイレクトの問題も直っています。

cross_origin_protectionは、6.7で既定で有効になる見込みです。今のうちに自分のアプリで有効にして、問題なく動くかを確かめておくと、次の版への移行が楽になります。

ここまでお読みいただきありがとうございました。

参考情報

次のアクション

記事で学んだ内容を実際に動かしてみよう

Python WebAcademyでは、ブラウザ上でコードを書きながら基礎から実践まで体系的に学べます。

Python WebAcademyの学習画面

あわせて読む

関連記事

ブログ一覧へ

Python学習ロードマップ

まずはこの3講座から

記事で気になったテーマを、順番に手を動かしながら学べます。

ロードマップを見る