Tencent/WeKnora · warning

URL rejected for security reasons: %v

Error message

URL rejected for security reasons: %v

What it means

downloadFromURL enforces SSRF protection: URLs not on the WeCom API host allowlist are passed to secutils.ValidateURLForSSRF, which rejects private/internal addresses. If that validation fails, the URL is rejected with 'URL rejected for security reasons: <reason>'. This stops attacker-controlled callback fields (e.g. a forged PicUrl) from making the server fetch internal resources.

Source

Thrown at internal/im/wecom/webhook_adapter.go:572

	if err != nil {
		return nil, "", fmt.Errorf("get access token: %w", err)
	}

	apiURL := fmt.Sprintf("%s/cgi-bin/media/get?access_token=%s&media_id=%s",
		a.apiBaseURL, accessToken, msg.FileKey)
	return downloadFromURL(ctx, apiURL, fileName, a.extraAllowedHost)
}

// downloadFromURL performs a GET request and returns the response body.
// It tries to resolve the real filename from HTTP response headers:
//  1. Content-Disposition: attachment; filename="xxx.pdf"
//  2. Content-Type → extension mapping (fallback for platforms like WeCom that
//     don't provide the original filename in the callback JSON)
func downloadFromURL(ctx context.Context, rawURL, fileName string, extraAllowedHost string) (io.ReadCloser, string, error) {
	// SSRF protection: reject internal/private URLs unless on the WeCom API allowlist.
	if !isAllowedIMAPIHost(rawURL, extraAllowedHost) {
		if err := secutils.ValidateURLForSSRF(rawURL); err != nil {
			return nil, "", fmt.Errorf("URL rejected for security reasons: %v", err)
		}
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
	if err != nil {
		return nil, "", fmt.Errorf("create request: %w", err)
	}

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, "", fmt.Errorf("download: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, "", fmt.Errorf("download failed: status=%d", resp.StatusCode)
	}

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Only download URLs that come from verified WeCom callbacks (msg_signature verified) and use official WeCom media hosts
  2. For tests/dev, pass the local host via the extraAllowedHost parameter of downloadFromURL / adapter configuration instead of bypassing validation
  3. If a new legitimate WeCom host is rejected, add it to the allowlist configuration — never disable SSRF validation
  4. Inspect the wrapped %v reason (e.g. private IP, bad scheme) to understand which rule fired

Example fix

// before: test fails against local server
rc, _, err := downloadFromURL(ctx, "http://localhost:8080/file", name, "")
// after
rc, _, err := downloadFromURL(ctx, "http://localhost:8080/file", name, "localhost:8080")
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(msg.FileKey)
if err != nil || (u.Scheme != "https" && u.Scheme != "http") || isPrivateHost(u.Hostname()) {
    return errors.New("untrusted file URL")
}

Type guard

func isPublicHTTPURL(raw string) bool {
    u, err := url.Parse(raw)
    return err == nil && (u.Scheme == "http" || u.Scheme == "https") &&
        net.ParseIP(u.Hostname()) != nil && !isPrivateIP(net.ParseIP(u.Hostname()))
}

Try / catch

rc, name, err := adapter.DownloadFile(ctx, msg)
if err != nil && strings.Contains(err.Error(), "URL rejected for security reasons") {
    logger.Warnf("blocked suspicious media URL: %v", err)
    return ErrUntrustedURL
}

Prevention

When it happens

Trigger: A message whose PicUrl/FileKey points to a private IP (10.x, 192.168.x, 169.254.169.254, localhost), a non-HTTP scheme, or any non-WeCom host not in extraAllowedHost — typically forged or replayed callbacks, or self-hosted test URLs.

Common situations: Local development pointing PicUrl at a local file server; test fixtures using example.com or internal hosts; genuinely malicious callbacks probing SSRF; WeCom changing media CDN hostnames so new hosts fail the allowlist check.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/beb6b28471be90ab. Report an issue: GitHub.