Skip to content

Commit 40dec17

Browse files
SnowballXueQiuhiifongwxiaoguang
authored
Fix Feishu webhook signature verification (#34788)
# Fix Feishu Webhook Signature Verification This PR implements proper signature verification for Feishu (Lark) webhooks according to the [official documentation](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot). ## Changes - Implemented the `GenSign` function based on Feishu's official Go sample code - Modified the webhook request creation to include timestamp and signature in the payload when a secret is configured - Fixed the signature generation algorithm to properly use HMAC-SHA256 with the correct string format ## Implementation Details The signature verification works as follows: 1. When a webhook secret is provided, a timestamp is generated 2. The signature string is created using `timestamp + "\n" + secret` 3. The HMAC-SHA256 algorithm is applied to an empty string using the signature string as the key 4. The result is Base64 encoded to produce the final signature 5. Both timestamp and signature are added to the payload According to Feishu's documentation, the timestamp must be within 1 hour (3600 seconds) of the current time to be considered valid. ## Security Note Feishu emphasizes the importance of keeping webhook URLs secure. Do not disclose them on GitHub, blogs, or any public sites to prevent unauthorized use. ## References - [Feishu Custom Bot Documentation](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot) --------- Co-authored-by: hiifong <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 90eb831 commit 40dec17

File tree

3 files changed

+39
-6
lines changed

3 files changed

+39
-6
lines changed

services/webhook/feishu.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ package webhook
55

66
import (
77
"context"
8+
"crypto/hmac"
9+
"crypto/sha256"
10+
"encoding/base64"
811
"fmt"
912
"net/http"
1013
"strings"
14+
"time"
1115

1216
webhook_model "code.gitea.io/gitea/models/webhook"
1317
"code.gitea.io/gitea/modules/git"
@@ -16,10 +20,12 @@ import (
1620
)
1721

1822
type (
19-
// FeishuPayload represents
23+
// FeishuPayload represents the payload for Feishu webhook
2024
FeishuPayload struct {
21-
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
22-
Content struct {
25+
Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
26+
Sign string `json:"sign,omitempty"` // Signature for verification
27+
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
28+
Content struct {
2329
Text string `json:"text"`
2430
} `json:"content"`
2531
}
@@ -184,9 +190,29 @@ func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, er
184190
return newFeishuTextPayload(text), nil
185191
}
186192

193+
// feishuGenSign generates a signature for Feishu webhook
194+
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
195+
func feishuGenSign(secret string, timestamp int64) string {
196+
// key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
197+
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
198+
h := hmac.New(sha256.New, []byte(stringToSign))
199+
return base64.StdEncoding.EncodeToString(h.Sum(nil))
200+
}
201+
187202
func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
188-
var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
189-
return newJSONRequest(pc, w, t, true)
203+
payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
204+
if err != nil {
205+
return nil, nil, err
206+
}
207+
208+
// Add timestamp and signature if secret is provided
209+
if w.Secret != "" {
210+
timestamp := time.Now().Unix()
211+
payload.Timestamp = timestamp
212+
payload.Sign = feishuGenSign(w.Secret, timestamp)
213+
}
214+
215+
return prepareJSONRequest(payload, w, t, false /* no default headers */)
190216
}
191217

192218
func init() {

services/webhook/feishu_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ func TestFeishuJSONPayload(t *testing.T) {
168168
URL: "https://feishu.example.com/",
169169
Meta: `{}`,
170170
HTTPMethod: "POST",
171+
Secret: "secret",
171172
}
172173
task := &webhook_model.HookTask{
173174
HookID: hook.ID,
@@ -183,10 +184,13 @@ func TestFeishuJSONPayload(t *testing.T) {
183184

184185
assert.Equal(t, "POST", req.Method)
185186
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
186-
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
187187
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
188188
var body FeishuPayload
189189
err = json.NewDecoder(req.Body).Decode(&body)
190190
assert.NoError(t, err)
191191
assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
192+
assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign)
193+
194+
// a separate sign test, the result is generated by official python code, so the algo must be correct
195+
assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1))
192196
}

services/webhook/payloader.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
9595
if err != nil {
9696
return nil, nil, err
9797
}
98+
return prepareJSONRequest(payload, w, t, withDefaultHeaders)
99+
}
98100

101+
func prepareJSONRequest[T any](payload T, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
99102
body, err := json.MarshalIndent(payload, "", " ")
100103
if err != nil {
101104
return nil, nil, err

0 commit comments

Comments
 (0)