From 02a05f06bd92d3eb48a334a024b8f41d021d68f3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 01:10:04 +0800 Subject: [PATCH] Add signature verification support for Feishu bot notifications (#2856) * Initial plan * Add signature verification support for Feishu bot notifications Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> * Add clarifying comments about Feishu signature algorithm Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> * Add i18n translations for larkSecret configuration field Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> --- back/data/notify.ts | 1 + back/services/notify.ts | 24 +++++++++++++++++++----- sample/notify.js | 20 ++++++++++++++++++-- sample/notify.py | 15 +++++++++++++++ src/locales/en-US.json | 1 + src/locales/zh-CN.json | 1 + src/utils/config.ts | 6 ++++++ 7 files changed, 61 insertions(+), 7 deletions(-) diff --git a/back/data/notify.ts b/back/data/notify.ts index bf4031d3..b0899500 100644 --- a/back/data/notify.ts +++ b/back/data/notify.ts @@ -142,6 +142,7 @@ export class WebhookNotification extends NotificationBaseInfo { export class LarkNotification extends NotificationBaseInfo { public larkKey = ''; + public larkSecret = ''; } export class NtfyNotification extends NotificationBaseInfo { diff --git a/back/services/notify.ts b/back/services/notify.ts index 4f4e2ebb..22609748 100644 --- a/back/services/notify.ts +++ b/back/services/notify.ts @@ -550,19 +550,33 @@ export default class NotificationService { } private async lark() { - let { larkKey } = this.params; + let { larkKey, larkSecret } = this.params; if (!larkKey.startsWith('http')) { larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`; } + const body: Record = { + msg_type: 'text', + content: { text: `${this.title}\n\n${this.content}` }, + }; + + // Add signature if secret is provided + // Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key + // and signs an empty message, which differs from typical HMAC usage + if (larkSecret) { + const timestamp = Math.floor(Date.now() / 1000).toString(); + const stringToSign = `${timestamp}\n${larkSecret}`; + const hmac = crypto.createHmac('sha256', stringToSign); + const sign = hmac.digest('base64'); + body.timestamp = timestamp; + body.sign = sign; + } + try { const res = await httpClient.post(larkKey, { ...this.gotOption, - json: { - msg_type: 'text', - content: { text: `${this.title}\n\n${this.content}` }, - }, + json: body, headers: { 'Content-Type': 'application/json' }, }); if (res.StatusCode === 0 || res.code === 0) { diff --git a/sample/notify.js b/sample/notify.js index 8dfe8851..49ae9f24 100644 --- a/sample/notify.js +++ b/sample/notify.js @@ -52,6 +52,7 @@ const push_config = { DD_BOT_TOKEN: '', // 钉钉机器人的 DD_BOT_TOKEN FSKEY: '', // 飞书机器人的 FSKEY + FSSECRET: '', // 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥 // 推送到个人QQ:http://127.0.0.1/send_private_msg // 群:http://127.0.0.1/send_group_msg @@ -989,11 +990,26 @@ function aibotkNotify(text, desp) { function fsBotNotify(text, desp) { return new Promise((resolve) => { - const { FSKEY } = push_config; + const { FSKEY, FSSECRET } = push_config; if (FSKEY) { + const body = { msg_type: 'text', content: { text: `${text}\n\n${desp}` } }; + + // Add signature if secret is provided + // Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key + // and signs an empty message, which differs from typical HMAC usage + if (FSSECRET) { + const crypto = require('crypto'); + const timestamp = Math.floor(Date.now() / 1000).toString(); + const stringToSign = `${timestamp}\n${FSSECRET}`; + const hmac = crypto.createHmac('sha256', stringToSign); + const sign = hmac.digest('base64'); + body.timestamp = timestamp; + body.sign = sign; + } + const options = { url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`, - json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } }, + json: body, headers: { 'Content-Type': 'application/json', }, diff --git a/sample/notify.py b/sample/notify.py index 032bb97e..8dd885ca 100644 --- a/sample/notify.py +++ b/sample/notify.py @@ -49,6 +49,7 @@ push_config = { 'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN 'FSKEY': '', # 飞书机器人的 FSKEY + 'FSSECRET': '', # 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥 'GOBOT_URL': '', # go-cqhttp # 推送到个人QQ:http://127.0.0.1/send_private_msg @@ -233,6 +234,20 @@ def feishu_bot(title: str, content: str) -> None: url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}' data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}} + + # Add signature if secret is provided + # Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key + # and signs an empty message, which differs from typical HMAC usage + if push_config.get("FSSECRET"): + timestamp = str(int(time.time())) + string_to_sign = f'{timestamp}\n{push_config.get("FSSECRET")}' + hmac_code = hmac.new( + string_to_sign.encode("utf-8"), digestmod=hashlib.sha256 + ).digest() + sign = base64.b64encode(hmac_code).decode("utf-8") + data["timestamp"] = timestamp + data["sign"] = sign + response = requests.post(url, data=json.dumps(data)).json() if response.get("StatusCode") == 0 or response.get("code") == 0: diff --git a/src/locales/en-US.json b/src/locales/en-US.json index bf4ec2dc..0a1a2a2a 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -389,6 +389,7 @@ "消息接收人": "message recipient", "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "Version, you can specify 'pro' for the Professional version and 'personal' for the Personal version. If left blank, it will default to the Professional version.", "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "Feishu group bot: https://www.feishu.cn/hc/zh-CN/articles/360024984973", + "飞书群组机器人加签密钥,安全设置中开启签名校验后获得": "Feishu group bot signature secret, obtained after enabling signature verification in security settings", "邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json": "Email service name, e.g., 126, 163, Gmail, QQ, etc. Supported list: https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json", "邮箱地址": "Email Address", "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index f69f3a29..448e140c 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -389,6 +389,7 @@ "消息接收人": "消息接收人", "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版", "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973", + "飞书群组机器人加签密钥,安全设置中开启签名校验后获得": "飞书群组机器人加签密钥,安全设置中开启签名校验后获得", "邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json": "邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json", "邮箱地址": "邮箱地址", "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定", diff --git a/src/utils/config.ts b/src/utils/config.ts index 49eab36b..b529a7d0 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -395,6 +395,12 @@ export default { ), required: true, }, + { + label: 'larkSecret', + tip: intl.get( + '飞书群组机器人加签密钥,安全设置中开启签名校验后获得', + ), + }, ], email: [ {