DifyでリプライメッセージをするLINE Botを作ってみましたが、これは署名検証がないのでなりすましも出来てしまいます。
署名検証
1. ユーザーがLINEにメッセージ
2. LINEのサーバー
3. 自分のアプリでデータを受ける
4. (検証) ← 本来は必要
5. リプライメッセージのAPIにリクエスト
2 => 3の部分でLINEのサーバーと自分アプリでデータが送られてきますが、5でリプライをする前の4でデータがしっかりLINEのサーバーから送られてきてるかを検証する必要があります。
Webhookトリガーで受ける際に
- ヘッダーに入っているX-Line-Signature
- 生データ文字列
- チャンネルシークレット
この3つを使って検証します。
Difyだと署名検証が出来なさそうだった
X-Line-Signatureは抜き出せますし、チャンネルシークレットも問題ないですが...
生データ文字列が厳しそうでした。
DifyのWebhookトリガーはデフォルトで_webhook_rawという変数にリクエストボディを Objectにしたデータ を送る形式になっていました。
結構試してたのですがうまくいかず。悩んでましたが厳しそう。
LINEのドキュメントによるとObjectにしたデータは厳しそう
こちらにあるように、LINEから送られてきたリクエストボディは 文字列でそのまま検証しないといけません。
なのに、Difyはオブジェクトで返してきます。
出力変数がobjectで固定となっているので、Webhookトリガーの中で処理が走ってしまってそうです。
コンテンツタイプ text/plainも厳しそう
コンテンツタイプの選択肢をtext/plainにしたら生データが送られてきそうな雰囲気はありますが、こちらに指定するとLINEからのメッセージが到達しませんでした。
別の検証サンプル
一応Node.jsで以下のコードで"生データ文字列"にて検証は出来ます。
const crypto = require("node:crypto");
const http = require("node:http");
const HOST = "api.line.me";
const REPLY_PATH = "/v2/bot/message/reply";
const CH_SECRET = process.env.CH_SECRET;
const CH_ACCESS_TOKEN = process.env.CH_ACCESS_TOKEN;
const PORT = 3000; //PORT
const httpClient = async (replyToken, SendMessageObject) => {
const REPLY_API_ENDPOINT = `https://${HOST}${REPLY_PATH}`;
const postDataStr = JSON.stringify({ replyToken, messages: SendMessageObject });
return fetch(REPLY_API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": `Bearer ${CH_ACCESS_TOKEN}`,
},
body: postDataStr,
});
};
const signatureValidation = (xLineSignature, channelSecret, body) => {
const originSignature = crypto
.createHmac("sha256", channelSecret)
.update(body, "utf8")
.digest("base64");
return xLineSignature === originSignature;
};
http.createServer((req, res) => {
const { headers, method, url } = req;
if (url !== "/" || method !== "POST") {
res.statusCode = 404;
return res.end();
}
let body = "";
req.on("data", chunk => (body += chunk));
req.on("end", async () => {
res.statusCode = 200;
res.end(); // 先に返す
const sig = headers["x-line-signature"] || headers["X-Line-Signature"];
if (!signatureValidation(sig, CH_SECRET, body)) return; // 署名認証エラー
// 署名認証OK
const events = JSON.parse(body).events || [];
await Promise.all(events.map(async (event) => {
if (event.type !== "message" || event.message.type !== "text") return;
await httpClient(event.replyToken, [{
type: "text",
quoteToken: event.message.quoteToken,
text: event.message.text
}]);
}));
});
}).listen(PORT);
以下のbodyがパースなどをせずの文字列になってないと上手くいきません。
const signatureValidation = (xLineSignature, channelSecret, body) => {
const originSignature = crypto
.createHmac("sha256", channelSecret)
.update(body, "utf8")
.digest("base64");
return xLineSignature === originSignature;
};
何らかの変更はダメらしいのでJSON.Stringify()はここでは利用不可です。
署名の検証を行う前に、署名やリクエストボディの文字列に何らかの変更(文字列の置換、デシリアライズ、エスケープ処理など)をしてしまうと、第三者によって改ざんされたリクエストと区別ができなくなり、署名の検証に失敗します。リクエストボディの文字列にバックスラッシュ(\)や改行(\n)といった特殊なエスケープ文字が含まれているか否かは関係ありません。どのようなリクエストであっても署名の検証を行うより前にデータを変更しないでください。
なのでDifyの中で見た際に最初からObjectで送られてきているともはや検証不可となります。
まとめ
現状Difyの素の機能では厳しそうということがわかりました。
Dify側のアップデートに期待する必要があるのかもしれません。
プロキシサーバーとか前処理があればまた違うのかもしれませんが、その辺までやるなら普通に組みますよね。
もしDifyの設定などでこう突破できるよって話を知っている人がいたら教えてください。




