全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / toi-toi-toi制作記 / Vol.33 toi-toi-toi制作記 Vol.6

記事 04

誰でも、なりすませた——Webhookに署名がなかった話

自分の書いたコードに、別のAIを当てた。Codexにセキュリティ監査をさせる。リリース前の、最後の確認のつもりだった。

2026-06-13 公開

自分の書いたコードに、別のAIを当てた。Codexにセキュリティ監査をさせる。リリース前の、最後の確認のつもりだった。

戻ってきた指摘は、三つとも「P1」だった。最優先。今すぐ直すべき、という重さの印。その中の一つが、背筋を冷やした。

「Webhookの署名を検証していない」


LINEからの通知を、無条件で信じていた

このシステムは、LINEから通知を受け取る。誰かが公式アカウントにメッセージを送ると、LINEがこちらのサーバーに「こういうイベントがありました」とPOSTしてくる。それを受けて、予約の処理や返信が動く。

問題は、そのPOSTを、中身も確かめずに信じていたことだった。

@app.post("/webhook/line")
async def webhook(request: Request):
    body = await request.body()
    data = json.loads(body)
    for event in data.get("events", []):
        ...

届いたJSONを、そのままLINEのイベントとして処理する。送り主がLINEかどうかを、一度も確かめていない。

つまり、LINEのふりをして、誰でもこのアドレスに偽のイベントを投げ込めた。試しに、署名なしのPOSTを送ってみた。サーバーは 200 OKstatus: ok と返した。にこやかに、偽物を受け取っていた。


署名は、送り主の指紋

LINEは、本物のリクエストに署名を付けて送ってくる。X-Line-Signature というヘッダーだ。

署名は、リクエストの本文と、自分だけが知っているチャネルシークレットから、計算で作られる。受け取った側は、同じ本文と同じシークレットで、同じ計算をやり直す。出てきた値が、ヘッダーの署名と一致すれば、それは確かにLINEが、その本文で送ったものだ。シークレットを知らない第三者には、正しい署名は作れない。

mac = hmac.new(CHANNEL_SECRET.encode(), body, hashlib.sha256).digest()
expected = base64.b64encode(mac).decode()
if not hmac.compare_digest(expected, signature):
    return JSONResponse({"error": "invalid signature"}, status_code=401)

一致しなければ、401で突き返す。本文を一文字でも書き換えれば、署名は合わなくなる。これで、本文と送り主の両方を、同時に確かめられる。


200が、401になった

直したあと、もう一度、署名なしのPOSTを投げた。

401。拒否された。正しい署名を付けて投げると、200。受け取った。にこやかに偽物を通していた入り口が、本物だけを通す入り口になった。

怖かったのは、これがずっと「動いていた」ことだ。署名を確かめなくても、LINEからの本物のリクエストは普通に処理される。表向き、何の問題もない。誰も偽物を投げてこない限り、欠陥は静かに眠っている。動いているからといって、守られているわけではなかった。

自分のコードは、自分には見えにくい。「ここは大丈夫だろう」という思い込みが、一番点検されない。別のAIに監査させてよかった、と心から思った。


外から来るものを信じる前に、それが本当に名乗っている相手なのかを確かめる。当たり前のことが、自分のコードでは抜けていた。

次回は、もう一つのP1——既知の固定パスワードと共有鍵を、本番を止めずに入れ替えた話を書く。

シンプルシステム株式会社 代表 伊藤勝彦