fix(acceptance): 允许受信模拟源的媒体物化
验收任务物化最终媒体时,仅允许访问 Acceptance Run 登记的协议模拟器精确 origin;其他私网、协议、端口及带 userinfo 的地址继续由 SSRF 防护拒绝。\n\n验证:Go 全量测试、go vet、gofmt 和 git diff --check 通过。
This commit is contained in:
@@ -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
|
||||
// the configured file-storage policy.
|
||||
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 {
|
||||
next, _, err := s.materializeLocalBinaryResult(ctx, taskID, result)
|
||||
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)
|
||||
sourceKey = decision.Inline.SourceKey
|
||||
} 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Service) uploadGeneratedURLAsset(ctx context.Context, taskID string, asset *generatedURLAsset, index int, channels []store.FileStorageChannel) (map[string]any, string, string, string, error) {
|
||||
payload, contentType, err := s.readGeneratedURLAsset(ctx, asset)
|
||||
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, acceptanceEmulatorBaseURL)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
@@ -834,7 +841,7 @@ func sanitizeFileExtension(value string) string {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -844,7 +851,8 @@ func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURL
|
||||
return nil, "", err
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
func generatedAssetHTTPClient(allowLocal bool) *http.Client {
|
||||
func generatedAssetHTTPClient(allowLocal bool, allowAcceptanceOrigin bool) *http.Client {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = nil
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
return ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user