fix: restrict payment return urls to internal result page

This commit is contained in:
IanShaw027
2026-04-21 14:10:30 +08:00
parent 62ff2d803f
commit 147ed42ad3
3 changed files with 63 additions and 9 deletions
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
@@ -16,6 +17,8 @@ import (
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
const paymentResultReturnPath = "/payment/result"
const (
PaymentSourceHostedRedirect = "hosted_redirect"
PaymentSourceWechatInAppResume = "wechat_in_app_resume"
@@ -215,7 +218,7 @@ func visibleMethodSourceSettingKey(method string) string {
}
}
func CanonicalizeReturnURL(raw string) (string, error) {
func CanonicalizeReturnURL(raw string, srcHost string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", nil
@@ -231,19 +234,29 @@ func CanonicalizeReturnURL(raw string) (string, error) {
if parsed.Path == "" {
parsed.Path = "/"
}
if parsed.Path != paymentResultReturnPath {
return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must target the canonical internal payment result page")
}
if !sameOriginHost(parsed.Host, srcHost) {
return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must use the same host as the current site")
}
return parsed.String(), nil
}
func buildPaymentReturnURL(base string, orderID int64, resumeToken string) (string, error) {
canonical, err := CanonicalizeReturnURL(base)
if err != nil || canonical == "" {
return canonical, err
canonical := strings.TrimSpace(base)
if canonical == "" {
return "", nil
}
parsed, err := url.Parse(canonical)
if err != nil {
return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must be a valid URL")
}
if !parsed.IsAbs() || parsed.Host == "" {
return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must be a valid absolute URL")
}
parsed.Fragment = ""
query := parsed.Query()
if orderID > 0 {
@@ -258,6 +271,31 @@ func buildPaymentReturnURL(base string, orderID int64, resumeToken string) (stri
return parsed.String(), nil
}
func sameOriginHost(returnURLHost string, requestHost string) bool {
returnHost := strings.TrimSpace(returnURLHost)
reqHost := strings.TrimSpace(requestHost)
if returnHost == "" || reqHost == "" {
return false
}
if strings.EqualFold(returnHost, reqHost) {
return true
}
returnName, returnPort := splitHostPortDefault(returnHost)
reqName, reqPort := splitHostPortDefault(reqHost)
if returnName == "" || reqName == "" {
return false
}
return strings.EqualFold(returnName, reqName) && returnPort == reqPort
}
func splitHostPortDefault(raw string) (string, string) {
if host, port, err := net.SplitHostPort(raw); err == nil {
return host, port
}
return raw, ""
}
func (s *PaymentResumeService) CreateToken(claims ResumeTokenClaims) (string, error) {
if err := s.ensureSigningKey(); err != nil {
return "", err