Skip to content

Commit 415c7d1

Browse files
SnowballXueQiuhiifongwxiaoguang
authored andcommitted
Fix Feishu webhook signature verification (go-gitea#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 acd4e10 commit 415c7d1

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
}
@@ -178,9 +184,29 @@ func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, er
178184
return newFeishuTextPayload(text), nil
179185
}
180186

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

186212
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
@@ -92,7 +92,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
9292
if err != nil {
9393
return nil, nil, err
9494
}
95+
return prepareJSONRequest(payload, w, t, withDefaultHeaders)
96+
}
9597

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

0 commit comments

Comments
 (0)