Skip to content

[security] fix(dingtalk): block SSRF in outbound media fetches#3569

Merged
Re-bin merged 1 commit into
HKUDS:mainfrom
Hinotoi-agent:fix/dingtalk-media-ssrf
May 1, 2026
Merged

[security] fix(dingtalk): block SSRF in outbound media fetches#3569
Re-bin merged 1 commit into
HKUDS:mainfrom
Hinotoi-agent:fix/dingtalk-media-ssrf

Conversation

@Hinotoi-agent
Copy link
Copy Markdown
Contributor

@Hinotoi-agent Hinotoi-agent commented May 1, 2026

Summary

This PR hardens DingTalk outbound media fetching so a remote media URL cannot make the nanobot host fetch internal HTTP resources and upload the returned bytes as DingTalk media.

The patch keeps direct remote media support, rejects redirects by default, and adds an explicit operator opt-in for redirect support. Even when redirects are enabled, same-host redirects are the only default; cross-host redirects require an explicit remote_media_redirect_allowed_hosts entry. Each redirect hop is validated before it is fetched, and the final response URL is validated before bytes are accepted.

Security issues covered

Issue Affected path Fix
DingTalk remote media SSRF/exfiltration DingTalkChannel._read_media_bytes() handling http:// / https:// media references Validate the initial URL, refuse redirects by default, restrict opt-in redirects to same-host or allowlisted hosts, validate each redirect hop/final URL, and cap downloaded bytes

Before this PR

  • DingTalk media references using http:// or https:// were fetched server-side.
  • The fetch used follow_redirects=True.
  • The DingTalk path did not call validate_url_target() or validate_resolved_url() before reading the response body.
  • The full remote response body was read into memory and then uploaded to DingTalk.

After this PR

  • Remote DingTalk media URLs are validated before any fetch.
  • Redirect responses are refused by default.
  • Operators who need same-host redirect support can opt in with allow_remote_media_redirects: true on the DingTalk channel config.
  • Cross-host redirects require an explicit remote_media_redirect_allowed_hosts entry.
  • Opt-in redirects are followed manually with follow_redirects=False, validating each Location target before fetching the next hop.
  • Redirect chains are capped at 3 hops.
  • The final response URL is validated before bytes are accepted.
  • Remote media downloads are capped at 20 MiB.
  • Focused regression tests cover private targets, private redirect results, redirect refusal by default, safe same-host redirect opt-in, cross-host redirect blocking, cross-host allowlist opt-in, private redirect blocking even when enabled, and oversized responses.

Why this matters

If an allowed control source or prompt-injection path can influence message(media=[...]), the DingTalk channel could previously be used as a host-side fetch primitive. That can expose internal HTTP resources because the fetched bytes are uploaded as DingTalk media.

This is a data-exfiltration SSRF risk in the DingTalk outbound media path. It is not framed as unauthenticated RCE or global compromise.

How this differs from related issue/PR

This patch is specific to DingTalk outbound media fetching.

Adjacent public hardening work such as DNS fail-closed validation and web-fetch URL sanitization protects other code paths, but this DingTalk sink still fetched remote media URLs directly from nanobot/channels/dingtalk.py. The fix here applies the network boundary to that channel-specific media-upload path and prevents redirect-based internal fetches before DingTalk upload.

Attack flow

  1. A permitted user/tool/prompt-injection path causes nanobot to send a DingTalk message with a remote media URL.
  2. The media URL points to an attacker-controlled HTTP endpoint.
  3. The endpoint redirects to an internal or loopback service, or the original URL directly targets an internal address.
  4. The nanobot host fetches the response bytes.
  5. The DingTalk channel uploads those bytes as media.

Affected code

Area File
DingTalk outbound media fetch nanobot/channels/dingtalk.py
DingTalk channel security regression tests tests/channels/test_dingtalk_channel.py

Root cause

  • Remote DingTalk media URLs crossed from user/tool-controlled content into host-side HTTP fetching.
  • The fetch did not enforce the project network validator boundary.
  • Redirects were followed automatically.
  • Response bodies were accepted without a media-size cap.

CVSS assessment

  • Issue: DingTalk outbound media SSRF/exfiltration
  • CVSS v3.1: 6.5 Medium
  • Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Rationale: exploitation requires an allowed control path that can cause DingTalk media sending, but the vulnerable fetch can disclose internal HTTP response bodies through DingTalk media upload. Integrity and availability impact are not required for the core issue.

Safe reproduction steps

On vulnerable code, a bounded local harness can reproduce the issue without contacting external services:

  1. Run a local redirector that returns 302 Location: http://127.0.0.1:<internal>/admin.txt.
  2. Run a local internal HTTP server that returns a known marker body.
  3. Call the DingTalk media send path with the redirector URL as the media reference.
  4. Observe that the internal marker body is read and passed to the upload path.

Expected vulnerable behavior

A vulnerable run fetches the redirector, follows the redirect to localhost, reads the internal response body, and uploads that body as DingTalk media.

Changes in this PR

  • Add allow_remote_media_redirects: false to the DingTalk channel config.
  • Add remote_media_redirect_allowed_hosts: [] for explicit cross-host redirect allowlisting.
  • Add DINGTALK_MAX_REMOTE_MEDIA_BYTES for remote DingTalk media downloads.
  • Add DINGTALK_MAX_REMOTE_MEDIA_REDIRECTS for bounded opt-in redirects.
  • Add a _fetch_remote_media_bytes() helper with SSRF, redirect, and size checks.
  • Validate the initial remote media URL using validate_url_target().
  • Fetch remote media with follow_redirects=False.
  • Reject redirects by default.
  • When allow_remote_media_redirects is enabled, resolve relative redirects, require same-host or allowlisted cross-host targets, validate each redirect target before fetching it, and stop after 3 redirects.
  • Validate the final response URL using validate_resolved_url().
  • Prefer streaming downloads with incremental byte counting.
  • Keep a fallback path for clients/test fakes that only implement .get().
  • Add regression tests for private targets, private redirects, redirect refusal, safe same-host redirect opt-in, cross-host redirect blocking, allowlisted cross-host redirect opt-in, private redirect blocking with opt-in enabled, and oversized responses.

Files changed

File Change
nanobot/channels/dingtalk.py Harden remote media fetch with URL validation, default redirect refusal, safe opt-in redirect validation, final URL validation, streaming, and size limit
tests/channels/test_dingtalk_channel.py Add DingTalk SSRF/redirect/size regression coverage and extend test fakes

Maintainer impact

  • Safe direct remote media URLs continue to work.
  • Redirecting remote media URLs are refused by default.
  • Deployments that need same-host redirect support can enable allow_remote_media_redirects: true for the DingTalk channel.
  • Deployments that need CDN/download redirects across hosts can add exact hostnames to remote_media_redirect_allowed_hosts.
  • Private/internal redirect targets remain blocked even when redirect support is enabled or allowlisted.
  • Remote DingTalk media bodies above 20 MiB are refused.
  • Local file and DingTalk download behavior are not intentionally changed.

Fix rationale

DingTalk media upload should not be a general-purpose server-side fetcher. Validating before fetch, refusing redirects by default, validating each opt-in redirect hop before it is fetched, restricting cross-host redirects to an explicit allowlist, validating the final response URL, and enforcing a byte cap keeps the trust boundary small and reviewable while preserving direct remote media support and providing an explicit compatibility setting for operators who need redirects.

Type of change

  • Security fix
  • Bug fix
  • Tests
  • Documentation update
  • Breaking change

Test plan

Validated locally with:

# repeated 3 times during the final verification loop
uv run --extra dev python -m ruff check nanobot/channels/dingtalk.py tests/channels/test_dingtalk_channel.py
uv run --extra dev python -m pytest tests/channels/test_dingtalk_channel.py tests/security/test_security_network.py -q
git diff --check

Result:

All checks passed!
40 passed in 0.19s

The final validation loop was run three times after the cross-host allowlist hardening; the DingTalk channel/security-network tests passed repeatedly.

Disclosure notes

This PR is intentionally bounded to DingTalk outbound media URL handling. It does not claim unauthenticated RCE, full remote compromise, or a global fix for every host-side fetch path. The core issue is SSRF/data exfiltration through DingTalk remote media fetching when a permitted control source can influence the media URL.

@Hinotoi-agent Hinotoi-agent force-pushed the fix/dingtalk-media-ssrf branch from cbb307b to bf58b79 Compare May 1, 2026 05:38
@Hinotoi-agent Hinotoi-agent force-pushed the fix/dingtalk-media-ssrf branch from bf58b79 to 634a6b7 Compare May 1, 2026 05:43
Copy link
Copy Markdown
Collaborator

@Re-bin Re-bin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #3569

This is a solid security hardening for DingTalk remote media fetching.

The fix keeps direct remote media support, blocks private/internal targets before fetch, disables redirects by default, validates opt-in redirect hops, restricts cross-host redirects to an explicit allowlist, validates the final URL, and caps downloaded bytes. The regression coverage matches the security boundary well.

I merged latest main locally and ran:

  • uv run pytest tests/channels/test_dingtalk_channel.py tests/security/test_security_network.py: 40 passed
  • uv run ruff check nanobot/channels/dingtalk.py tests/channels/test_dingtalk_channel.py: passed

I also recorded the new DingTalk config/docs follow-up in the local web-docs todo. Ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants