Skip to content

Fix Feishu webhook signature verification (#34788) #34802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: release/v1.24
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions services/webhook/feishu.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ package webhook

import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
Expand All @@ -16,10 +20,12 @@ import (
)

type (
// FeishuPayload represents
// FeishuPayload represents the payload for Feishu webhook
FeishuPayload struct {
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
Content struct {
Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
Sign string `json:"sign,omitempty"` // Signature for verification
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
Content struct {
Text string `json:"text"`
} `json:"content"`
}
Expand Down Expand Up @@ -178,9 +184,29 @@ func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, er
return newFeishuTextPayload(text), nil
}

// feishuGenSign generates a signature for Feishu webhook
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
func feishuGenSign(secret string, timestamp int64) string {
// key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
h := hmac.New(sha256.New, []byte(stringToSign))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
return newJSONRequest(pc, w, t, true)
payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
if err != nil {
return nil, nil, err
}

// Add timestamp and signature if secret is provided
if w.Secret != "" {
timestamp := time.Now().Unix()
payload.Timestamp = timestamp
payload.Sign = feishuGenSign(w.Secret, timestamp)
}

return prepareJSONRequest(payload, w, t, false /* no default headers */)
}

func init() {
Expand Down
6 changes: 5 additions & 1 deletion services/webhook/feishu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func TestFeishuJSONPayload(t *testing.T) {
URL: "https://feishu.example.com/",
Meta: `{}`,
HTTPMethod: "POST",
Secret: "secret",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
Expand All @@ -183,10 +184,13 @@ func TestFeishuJSONPayload(t *testing.T) {

assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body FeishuPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
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)
assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign)

// a separate sign test, the result is generated by official python code, so the algo must be correct
assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1))
}
3 changes: 3 additions & 0 deletions services/webhook/payloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
if err != nil {
return nil, nil, err
}
return prepareJSONRequest(payload, w, t, withDefaultHeaders)
}

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