fix(acceptance): 允许受信模拟源的媒体物化
验收任务物化最终媒体时,仅允许访问 Acceptance Run 登记的协议模拟器精确 origin;其他私网、协议、端口及带 userinfo 的地址继续由 SSRF 防护拒绝。\n\n验证:Go 全量测试、go vet、gofmt 和 git diff --check 通过。
This commit is contained in:
@@ -281,7 +281,7 @@ func (s *Service) readVideoInputImageBytes(ctx context.Context, source string) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("图片地址无效")
|
return nil, fmt.Errorf("图片地址无效")
|
||||||
}
|
}
|
||||||
httpClient := generatedAssetHTTPClient(false)
|
httpClient := generatedAssetHTTPClient(false, false)
|
||||||
httpClient.Timeout = 10 * time.Second
|
httpClient.Timeout = 10 * time.Second
|
||||||
httpClient.CheckRedirect = func(request *http.Request, via []*http.Request) error {
|
httpClient.CheckRedirect = func(request *http.Request, via []*http.Request) error {
|
||||||
if len(via) >= 3 {
|
if len(via) >= 3 {
|
||||||
|
|||||||
@@ -129,6 +129,13 @@ func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, task
|
|||||||
// override scoped to the authenticated run so normal production tasks retain
|
// override scoped to the authenticated run so normal production tasks retain
|
||||||
// the configured file-storage policy.
|
// the configured file-storage policy.
|
||||||
policy = generatedAssetUploadPolicyForAcceptanceRun(policy, acceptanceRunID)
|
policy = generatedAssetUploadPolicyForAcceptanceRun(policy, acceptanceRunID)
|
||||||
|
acceptanceEmulatorBaseURL := ""
|
||||||
|
if strings.TrimSpace(acceptanceRunID) != "" {
|
||||||
|
acceptanceEmulatorBaseURL, _, err = s.store.AcceptanceCandidateOverride(ctx, acceptanceRunID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &clients.ClientError{Code: "acceptance_run_inactive", Message: err.Error(), Retryable: false}
|
||||||
|
}
|
||||||
|
}
|
||||||
if policy.LocalizeInlineMedia {
|
if policy.LocalizeInlineMedia {
|
||||||
next, _, err := s.materializeLocalBinaryResult(ctx, taskID, result)
|
next, _, err := s.materializeLocalBinaryResult(ctx, taskID, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -210,7 +217,7 @@ func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, task
|
|||||||
upload, contentType, kind, strategy, err = s.uploadGeneratedAsset(ctx, taskID, decision.Inline, index, channels)
|
upload, contentType, kind, strategy, err = s.uploadGeneratedAsset(ctx, taskID, decision.Inline, index, channels)
|
||||||
sourceKey = decision.Inline.SourceKey
|
sourceKey = decision.Inline.SourceKey
|
||||||
} else {
|
} else {
|
||||||
upload, contentType, kind, strategy, err = s.uploadGeneratedURLAsset(ctx, taskID, decision.URL, index, channels)
|
upload, contentType, kind, strategy, err = s.uploadGeneratedURLAsset(ctx, taskID, decision.URL, index, channels, acceptanceEmulatorBaseURL)
|
||||||
sourceKey = decision.URL.SourceKey
|
sourceKey = decision.URL.SourceKey
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -687,8 +694,8 @@ func (s *Service) uploadGeneratedAsset(ctx context.Context, taskID string, asset
|
|||||||
return upload, contentType, kind, "upload_inline_media", err
|
return upload, contentType, kind, "upload_inline_media", err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) uploadGeneratedURLAsset(ctx context.Context, taskID string, asset *generatedURLAsset, index int, channels []store.FileStorageChannel) (map[string]any, string, string, string, error) {
|
func (s *Service) uploadGeneratedURLAsset(ctx context.Context, taskID string, asset *generatedURLAsset, index int, channels []store.FileStorageChannel, acceptanceEmulatorBaseURL string) (map[string]any, string, string, string, error) {
|
||||||
payload, contentType, err := s.readGeneratedURLAsset(ctx, asset)
|
payload, contentType, err := s.readGeneratedURLAsset(ctx, asset, acceptanceEmulatorBaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", "", "", err
|
return nil, "", "", "", err
|
||||||
}
|
}
|
||||||
@@ -834,7 +841,7 @@ func sanitizeFileExtension(value string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURLAsset) ([]byte, string, error) {
|
func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURLAsset, acceptanceEmulatorBaseURL string) ([]byte, string, error) {
|
||||||
fetchURL, err := s.generatedAssetFetchURL(asset.URL)
|
fetchURL, err := s.generatedAssetFetchURL(asset.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
@@ -844,7 +851,8 @@ func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURL
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
allowLocal := strings.HasPrefix(strings.TrimSpace(asset.URL), "/")
|
allowLocal := strings.HasPrefix(strings.TrimSpace(asset.URL), "/")
|
||||||
resp, err := generatedAssetHTTPClient(allowLocal).Do(req)
|
allowAcceptanceOrigin := sameHTTPOrigin(asset.URL, acceptanceEmulatorBaseURL)
|
||||||
|
resp, err := generatedAssetHTTPClient(allowLocal, allowAcceptanceOrigin).Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", &clients.ClientError{Code: "upload_source_fetch_failed", Message: err.Error(), Retryable: true}
|
return nil, "", &clients.ClientError{Code: "upload_source_fetch_failed", Message: err.Error(), Retryable: true}
|
||||||
}
|
}
|
||||||
@@ -873,7 +881,7 @@ func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURL
|
|||||||
return payload, strings.TrimSpace(strings.Split(contentType, ";")[0]), nil
|
return payload, strings.TrimSpace(strings.Split(contentType, ";")[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generatedAssetHTTPClient(allowLocal bool) *http.Client {
|
func generatedAssetHTTPClient(allowLocal bool, allowAcceptanceOrigin bool) *http.Client {
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
transport.Proxy = nil
|
transport.Proxy = nil
|
||||||
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
@@ -886,7 +894,8 @@ func generatedAssetHTTPClient(allowLocal bool) *http.Client {
|
|||||||
return nil, errors.New("generated media DNS resolution failed")
|
return nil, errors.New("generated media DNS resolution failed")
|
||||||
}
|
}
|
||||||
for _, address := range addresses {
|
for _, address := range addresses {
|
||||||
if generatedAssetBlockedAddress(address.IP) && !(allowLocal && address.IP.IsLoopback()) {
|
if generatedAssetBlockedAddress(address.IP) &&
|
||||||
|
!(allowLocal && address.IP.IsLoopback()) && !allowAcceptanceOrigin {
|
||||||
return nil, errors.New("generated media resolved to a blocked network")
|
return nil, errors.New("generated media resolved to a blocked network")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -910,6 +919,18 @@ func generatedAssetHTTPClient(allowLocal bool) *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sameHTTPOrigin(rawURL string, allowedBaseURL string) bool {
|
||||||
|
raw, err := url.Parse(strings.TrimSpace(rawURL))
|
||||||
|
if err != nil || raw.User != nil || (raw.Scheme != "http" && raw.Scheme != "https") || raw.Host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
allowed, err := url.Parse(strings.TrimSpace(allowedBaseURL))
|
||||||
|
if err != nil || allowed.User != nil || (allowed.Scheme != "http" && allowed.Scheme != "https") || allowed.Host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.EqualFold(raw.Scheme, allowed.Scheme) && strings.EqualFold(raw.Host, allowed.Host)
|
||||||
|
}
|
||||||
|
|
||||||
func generatedAssetBlockedAddress(ip net.IP) bool {
|
func generatedAssetBlockedAddress(ip net.IP) bool {
|
||||||
return ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast()
|
return ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -301,6 +303,32 @@ func TestAcceptanceRunForcesURLMaterializationOnlyForAcceptanceTask(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAcceptanceGeneratedMediaAllowsOnlyExactEmulatorOrigin(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "video/mp4")
|
||||||
|
_, _ = w.Write([]byte{0, 0, 0, 16, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
service := &Service{}
|
||||||
|
asset := &generatedURLAsset{URL: server.URL + "/media/result.mp4", Kind: "video"}
|
||||||
|
payload, contentType, err := service.readGeneratedURLAsset(t.Context(), asset, server.URL+"/v1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read exact acceptance origin: %v", err)
|
||||||
|
}
|
||||||
|
if len(payload) != 12 || contentType != "video/mp4" {
|
||||||
|
t.Fatalf("payload=%d contentType=%q", len(payload), contentType)
|
||||||
|
}
|
||||||
|
for _, allowed := range []string{
|
||||||
|
strings.Replace(server.URL, "http://", "https://", 1),
|
||||||
|
server.URL + "1",
|
||||||
|
strings.Replace(server.URL, "http://", "http://user@", 1),
|
||||||
|
} {
|
||||||
|
if sameHTTPOrigin(asset.URL, allowed) {
|
||||||
|
t.Fatalf("unexpected origin match: %q", allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFinalizeGeneratedAssetsUploadsNestedInlineBinaryUnderDefaultPolicy(t *testing.T) {
|
func TestFinalizeGeneratedAssetsUploadsNestedInlineBinaryUnderDefaultPolicy(t *testing.T) {
|
||||||
storageDir := t.TempDir()
|
storageDir := t.TempDir()
|
||||||
service := &Service{cfg: config.Config{LocalGeneratedStorageDir: storageDir}}
|
service := &Service{cfg: config.Config{LocalGeneratedStorageDir: storageDir}}
|
||||||
|
|||||||
Reference in New Issue
Block a user