兼容开放导入未签名请求

This commit is contained in:
yml2213
2026-06-11 07:32:11 +08:00
parent 88b1df64e7
commit 7130135e23
5 changed files with 37 additions and 17 deletions
+2 -2
View File
@@ -56,8 +56,8 @@ REALNAME_CLOUDMARKET_APPCODE=
# 生成方式:openssl rand -hex 16 # 生成方式:openssl rand -hex 16
PAYMENT_CONFIG_ENCRYPTION_KEY= PAYMENT_CONFIG_ENCRYPTION_KEY=
# 开放导入接口签名密钥;留空时 /api/open/listing-uploads 不可用 # 开放导入接口可选签名密钥;留空或请求未带签名时按旧方式导入
# 请求需携带 X-HFB-Timestamp 和 X-HFB-Signature。 # 若调用方携带 X-HFB-Timestamp 和 X-HFB-Signature,则会校验 HMAC 签名
EXTERNAL_UPLOAD_SECRET= EXTERNAL_UPLOAD_SECRET=
# 可选:逗号分隔的 IP 或 CIDR 白名单,例如 127.0.0.1,10.0.0.0/8。 # 可选:逗号分隔的 IP 或 CIDR 白名单,例如 127.0.0.1,10.0.0.0/8。
EXTERNAL_UPLOAD_ALLOWED_IPS= EXTERNAL_UPLOAD_ALLOWED_IPS=
+3 -2
View File
@@ -62,7 +62,8 @@ REALNAME_CLOUDMARKET_APPCODE=
# 警告:此密钥一旦设置不要更改,否则已有配置无法解密 # 警告:此密钥一旦设置不要更改,否则已有配置无法解密
PAYMENT_CONFIG_ENCRYPTION_KEY=change-to-32-byte-encryption-key PAYMENT_CONFIG_ENCRYPTION_KEY=change-to-32-byte-encryption-key
# 开放导入接口签名密钥,生产环境必填;生成方式:openssl rand -hex 32 # 开放导入接口可选签名密钥;内部调用方暂不签名时可留空。
EXTERNAL_UPLOAD_SECRET=change-to-a-long-random-upload-secret # 若启用签名,生成方式:openssl rand -hex 32
EXTERNAL_UPLOAD_SECRET=
# 可选:逗号分隔的 IP 或 CIDR 白名单,例如 203.0.113.10,10.0.0.0/8。 # 可选:逗号分隔的 IP 或 CIDR 白名单,例如 203.0.113.10,10.0.0.0/8。
EXTERNAL_UPLOAD_ALLOWED_IPS= EXTERNAL_UPLOAD_ALLOWED_IPS=
-3
View File
@@ -120,9 +120,6 @@ func (c Config) ValidateProductionSecurity() error {
if isPlaceholder(c.PaymentConfigEncryptionKey) || (keyLen != 16 && keyLen != 24 && keyLen != 32) { if isPlaceholder(c.PaymentConfigEncryptionKey) || (keyLen != 16 && keyLen != 24 && keyLen != 32) {
return errors.New("PAYMENT_CONFIG_ENCRYPTION_KEY must be 16, 24, or 32 bytes in production") return errors.New("PAYMENT_CONFIG_ENCRYPTION_KEY must be 16, 24, or 32 bytes in production")
} }
if strings.TrimSpace(c.ExternalUploadSecret) == "" || isPlaceholder(c.ExternalUploadSecret) {
return errors.New("EXTERNAL_UPLOAD_SECRET is required in production")
}
if c.BootstrapAdminPassword != "" && isPlaceholder(c.BootstrapAdminPassword) { if c.BootstrapAdminPassword != "" && isPlaceholder(c.BootstrapAdminPassword) {
return errors.New("ADMIN_BOOTSTRAP_PASSWORD must not use the example placeholder in production") return errors.New("ADMIN_BOOTSTRAP_PASSWORD must not use the example placeholder in production")
} }
@@ -47,7 +47,6 @@ func (h *Handler) ImportExternalUpload(c *gin.Context) {
} }
var ( var (
errExternalUploadSecretMissing = errors.New("external upload secret missing")
errExternalUploadForbiddenIP = errors.New("external upload forbidden ip") errExternalUploadForbiddenIP = errors.New("external upload forbidden ip")
errExternalUploadTimestamp = errors.New("external upload timestamp invalid") errExternalUploadTimestamp = errors.New("external upload timestamp invalid")
errExternalUploadSignature = errors.New("external upload signature invalid") errExternalUploadSignature = errors.New("external upload signature invalid")
@@ -56,15 +55,18 @@ var (
const externalUploadMaxClockSkew = 5 * time.Minute const externalUploadMaxClockSkew = 5 * time.Minute
func (h *Handler) verifyExternalUpload(c *gin.Context, raw []byte) error { func (h *Handler) verifyExternalUpload(c *gin.Context, raw []byte) error {
secret := strings.TrimSpace(h.externalUploadSecret)
if secret == "" {
return errExternalUploadSecretMissing
}
if !externalUploadIPAllowed(c.ClientIP(), h.externalUploadAllowedIPs) { if !externalUploadIPAllowed(c.ClientIP(), h.externalUploadAllowedIPs) {
return errExternalUploadForbiddenIP return errExternalUploadForbiddenIP
} }
secret := strings.TrimSpace(h.externalUploadSecret)
if secret == "" {
return nil
}
timestamp := strings.TrimSpace(c.GetHeader("X-HFB-Timestamp")) timestamp := strings.TrimSpace(c.GetHeader("X-HFB-Timestamp"))
signature := strings.TrimSpace(c.GetHeader("X-HFB-Signature")) signature := strings.TrimSpace(c.GetHeader("X-HFB-Signature"))
if timestamp == "" && signature == "" {
return nil
}
if timestamp == "" || signature == "" { if timestamp == "" || signature == "" {
return errExternalUploadSignature return errExternalUploadSignature
} }
@@ -111,8 +113,6 @@ func externalUploadIPAllowed(clientIP string, allowed []string) bool {
func writeExternalUploadAuthError(c *gin.Context, err error) { func writeExternalUploadAuthError(c *gin.Context, err error) {
switch err { switch err {
case errExternalUploadSecretMissing:
response.ServiceUnavailable(c, "开放导入签名密钥未配置")
case errExternalUploadForbiddenIP: case errExternalUploadForbiddenIP:
response.Error(c, http.StatusForbidden, "forbidden", "来源 IP 不允许访问") response.Error(c, http.StatusForbidden, "forbidden", "来源 IP 不允许访问")
case errExternalUploadTimestamp: case errExternalUploadTimestamp:
@@ -30,6 +30,28 @@ func TestVerifyExternalUploadSignature(t *testing.T) {
} }
} }
func TestVerifyExternalUploadAllowsUnsignedRequest(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("POST", "/api/open/listing-uploads", nil)
handler := &Handler{}
if err := handler.verifyExternalUpload(c, []byte(`{}`)); err != nil {
t.Fatalf("verifyExternalUpload() error = %v", err)
}
}
func TestVerifyExternalUploadAllowsUnsignedRequestWithSecret(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("POST", "/api/open/listing-uploads", nil)
handler := &Handler{externalUploadSecret: "test-upload-secret"}
if err := handler.verifyExternalUpload(c, []byte(`{}`)); err != nil {
t.Fatalf("verifyExternalUpload() error = %v", err)
}
}
func TestVerifyExternalUploadRejectsBadSignature(t *testing.T) { func TestVerifyExternalUploadRejectsBadSignature(t *testing.T) {
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder()) c, _ := gin.CreateTestContext(httptest.NewRecorder())