(security): auto-generate webhook secret when unset, redact token in tgapi standalone logs (fix): webhookLogger nil panic, dead warning block (tests): update secret-path test for new auto-gen behaviour (doc): CHANGELOG v1.0.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.1
|
||||
|
||||
### Fixed
|
||||
- Fixed webhook always accepting unauthenticated requests when `SecretToken` is not configured. A cryptographically random 32-byte token is now generated automatically when `SecretToken` is empty, so the webhook endpoint is always authenticated. The generated token is logged as a warning so the operator can record it.
|
||||
- Fixed `tgapi.NewAPI` and `tgapi.NewUploader` not installing token redaction on their managed loggers. The bot token is now masked as `<TOKEN>` in debug output even when the `tgapi` package is used standalone without the `laniakea.Bot` wrapper.
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
+11
-6
@@ -137,6 +137,15 @@ func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOp
|
||||
if len(bot.plugins) == 0 {
|
||||
return ErrNoPlugins
|
||||
}
|
||||
autoSecret := ""
|
||||
if opts.SecretToken == "" {
|
||||
rndSecret, err := generateToken(32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.SecretToken = rndSecret
|
||||
autoSecret = rndSecret
|
||||
}
|
||||
if opts.URL == "" {
|
||||
return ErrNoBotWebhookOptsURL
|
||||
}
|
||||
@@ -146,9 +155,6 @@ func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOp
|
||||
if err := validateWebhookPath(opts.Path, opts.UseStatusPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.UseStatusPath && opts.SecretToken == "" {
|
||||
return ErrStatusPathSecretRequired
|
||||
}
|
||||
if err := validateWebhookTLSFiles(tlsFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,10 +164,9 @@ func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOp
|
||||
}
|
||||
|
||||
return bot.runWebhookRuntime(ctx, func(runCtx context.Context) error {
|
||||
if opts.SecretToken == "" {
|
||||
bot.webhookLogger.Warnln("Using webhook without secret is very dangerous. Anyone can simulate Telegram requests.")
|
||||
if autoSecret != "" {
|
||||
bot.webhookLogger.Warnln("Using webhook without secret is very dangerous. Using random 32 bytes token:", autoSecret)
|
||||
}
|
||||
|
||||
i, err := bot.api.GetWebhookInfoWithContext(runCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+8
-8
@@ -350,20 +350,20 @@ func TestRunWebhookWithContextRejectsInvalidTLSFilesBeforeRemoteSetup(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWebhookWithContextRequiresSecretWhenStatusPathEnabled(t *testing.T) {
|
||||
func TestRunWebhookWithContextAutoGeneratesSecretWhenEmpty(t *testing.T) {
|
||||
bot := &Bot[NoData]{
|
||||
prefixes: []string{"/"},
|
||||
plugins: []Plugin[NoData]{{name: "demo"}},
|
||||
}
|
||||
opts := NewBotWebhookOpts().
|
||||
SetURL("https://bot.example.com").
|
||||
SetUseStatusPath(true)
|
||||
// No SecretToken, no URL — function should auto-generate the token
|
||||
// and then fail with ErrNoBotWebhookOptsURL before any network call.
|
||||
opts := NewBotWebhookOpts().SetUseStatusPath(true)
|
||||
|
||||
err := bot.RunWebhookWithContext(context.Background(), opts)
|
||||
if err == nil {
|
||||
t.Fatal("expected status-path secret validation error, got nil")
|
||||
if !errors.Is(err, ErrNoBotWebhookOptsURL) {
|
||||
t.Fatalf("expected ErrNoBotWebhookOptsURL after auto-generation, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "SecretToken required") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
if opts.SecretToken == "" {
|
||||
t.Fatal("expected SecretToken to be auto-generated, got empty string")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ func NewAPI(opts *APIOpts) *API {
|
||||
"API", utils.GetLoggerLevel(),
|
||||
opts.logFormat, opts.logFormatter,
|
||||
)
|
||||
logger.AddReplacer(opts.token, "<TOKEN>")
|
||||
|
||||
client := opts.client
|
||||
if client == nil {
|
||||
|
||||
@@ -79,6 +79,7 @@ func NewUploader(api *API) *Uploader {
|
||||
"UPLOADER", utils.GetLoggerLevel(),
|
||||
api.logFormat, api.logFormatter,
|
||||
)
|
||||
logger.AddReplacer(api.token, "<TOKEN>")
|
||||
return &Uploader{api, logger}
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("Laniakea/%s", utils.VersionString))
|
||||
req.ContentLength = int64(buf.Len())
|
||||
|
||||
up.logger.Debugln("UPLOADER REQ", r.method)
|
||||
up.logger.Debugln("UPLOADER REQ", url)
|
||||
resp, err := up.api.client.Do(req)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
@@ -154,7 +155,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
up.logger.Debugln("UPLOADER RES", r.method, string(body))
|
||||
up.logger.Debugln("UPLOADER RES", url, string(body))
|
||||
|
||||
response, err := parseBody[R](body)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package laniakea
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||
)
|
||||
|
||||
@@ -27,3 +30,11 @@ const (
|
||||
// VersionBeta re-exports the module prerelease counter.
|
||||
VersionBeta = utils.VersionBeta
|
||||
)
|
||||
|
||||
func generateToken(b int) (string, error) {
|
||||
bytes := make([]byte, b)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,13 +2,13 @@ package utils
|
||||
|
||||
const (
|
||||
// VersionString is the module version string.
|
||||
VersionString = "1.0.0"
|
||||
VersionString = "1.0.1"
|
||||
// VersionMajor is the module major version.
|
||||
VersionMajor = 1
|
||||
// VersionMinor is the module minor version.
|
||||
VersionMinor = 0
|
||||
// VersionPatch is the module patch version.
|
||||
VersionPatch = 0
|
||||
VersionPatch = 1
|
||||
// VersionBeta is the prerelease counter for the current version.
|
||||
VersionBeta = 0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user