Tencent/WeKnora · error
attachment URL rejected: %w
Error message
attachment URL rejected: %w
What it means
DownloadFile downloads an attachment (typically a Notion S3 signed URL) but first runs the URL through utils.ValidateURLForSSRF, which blocks URLs pointing at private/loopback/link-local addresses or disallowed schemes. If validation fails, the download is refused with 'attachment URL rejected' wrapping the SSRF validator's reason. This is a deliberate security guard against server-side request forgery via attacker-controllable attachment URLs.
Source
Thrown at internal/datasource/connector/notion/client.go:383
func (c *notionClient) ResolveBlock(ctx context.Context, blockID string) (*notionBlock, error) {
respBody, err := c.doRequest(ctx, http.MethodGet, "/v1/blocks/"+blockID, nil)
if err != nil {
return nil, err
}
var block notionBlock
if err := json.Unmarshal(respBody, &block); err != nil {
return nil, fmt.Errorf("unmarshal block: %w", err)
}
return &block, nil
}
const maxDownloadSize = 100 * 1024 * 1024 // 100MB — prevent OOM from oversized files
// DownloadFile downloads a file from the given URL (typically an S3 signed URL).
// Does not go through the rate limiter since it's not a Notion API call.
func (c *notionClient) DownloadFile(ctx context.Context, fileURL string) ([]byte, error) {
if err := utils.ValidateURLForSSRF(fileURL); err != nil {
return nil, fmt.Errorf("attachment URL rejected: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
if err != nil {
return nil, fmt.Errorf("create download request: %w", err)
}
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = err
if attempt < maxRetries {
if sErr := sleepWithContext(ctx, time.Duration(1<<attempt)*time.Second); sErr != nil {
return nil, sErr
}
continue
}
breakView on GitHub (pinned to 988cbb0330)
Solutions
- Inspect the wrapped validator message to see which rule fired (scheme, IP range, or hostname) and confirm the attachment URL is a legitimate public Notion/S3 signed URL.
- Re-sync the page so Notion re-resolves file_upload blocks into fresh, valid S3 URLs (ResolveBlock); stale or hand-crafted URLs are the usual culprit.
- If the file is genuinely on an internal mirror, download it outside this library or add a narrowly scoped, reviewed allowance to the SSRF validator config.
- Never bypass the SSRF check for user-supplied URLs — if you must fetch an internal asset, do it in a separate, network-isolated worker with an explicit allowlist.
Example fix
// before client.DownloadFile(ctx, "http://192.168.1.10/files/doc.pdf") // after client.DownloadFile(ctx, block.File.File.URL) // public S3 signed URL from the Notion block
Defensive patterns
Strategy: validation
Validate before calling
u, err := url.Parse(fileURL)
if err != nil || (u.Scheme != "https" && u.Scheme != "http") {
return fmt.Errorf("attachment url must be absolute http(s): %q", fileURL)
}
if ip := net.ParseIP(u.Hostname()); ip != nil && (ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()) {
return fmt.Errorf("attachment url points at private address")
} Type guard
func isPublicHTTPURL(raw string) bool {
u, err := url.Parse(raw)
if err != nil || u.Hostname() == "" {
return false
}
if u.Scheme != "https" && u.Scheme != "http" {
return false
}
ip := net.ParseIP(u.Hostname())
return ip == nil || !(ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast())
} Try / catch
data, err := client.DownloadFile(ctx, fileURL)
if err != nil {
var rejection = "attachment URL rejected"
if strings.Contains(err.Error(), rejection) {
logger.Warnf(ctx, "skipping attachment %s: SSRF validation failed", fileURL)
return nil // do not retry; the URL is not fetchable by policy
}
return err
} Prevention
- Only download from the signed URLs Notion returns in block payloads; never accept user-supplied attachment URLs.
- Re-resolve file_upload blocks (ResolveBlock) instead of reconstructing or mirroring URLs yourself.
- Treat rejection errors as permanent — do not retry them.
When it happens
Trigger: Calling DownloadFile with a fileURL that resolves to a private IP (10.x, 192.168.x, 127.0.0.1, 169.254.169.254 metadata endpoints), a non-http(s) scheme (file://, gopher://), or a hostname that fails the SSRF validator — often because a page/block contains a forged or non-Notion external file URL.
Common situations: Notion pages containing external file links (not S3-uploaded files) that point at internal hosts; self-hosted environments where attachment storage was remapped to an internal mirror; DNS that resolves a public-looking hostname to a private address; accidentally passing a local file path as the URL.
Related errors
- URL rejected for security reasons: %v
- unsafe MinIO endpoint: %w
- unsafe OSS endpoint: %w
- invalid file path: %w
- unsafe S3 endpoint: %w
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/6baf256f58ced1ee.
Report an issue: GitHub.