[security] fix(dingtalk): block SSRF in outbound media fetches#3569
Merged
Conversation
cbb307b to
bf58b79
Compare
bf58b79 to
634a6b7
Compare
Re-bin
approved these changes
May 1, 2026
Collaborator
Re-bin
left a comment
There was a problem hiding this comment.
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 passeduv 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_hostsentry. Each redirect hop is validated before it is fetched, and the final response URL is validated before bytes are accepted.Security issues covered
DingTalkChannel._read_media_bytes()handlinghttp:///https://media referencesBefore this PR
http://orhttps://were fetched server-side.follow_redirects=True.validate_url_target()orvalidate_resolved_url()before reading the response body.After this PR
allow_remote_media_redirects: trueon the DingTalk channel config.remote_media_redirect_allowed_hostsentry.follow_redirects=False, validating eachLocationtarget before fetching the next hop.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
Affected code
nanobot/channels/dingtalk.pytests/channels/test_dingtalk_channel.pyRoot cause
CVSS assessment
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NRationale: 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:
302 Location: http://127.0.0.1:<internal>/admin.txt.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
allow_remote_media_redirects: falseto the DingTalk channel config.remote_media_redirect_allowed_hosts: []for explicit cross-host redirect allowlisting.DINGTALK_MAX_REMOTE_MEDIA_BYTESfor remote DingTalk media downloads.DINGTALK_MAX_REMOTE_MEDIA_REDIRECTSfor bounded opt-in redirects._fetch_remote_media_bytes()helper with SSRF, redirect, and size checks.validate_url_target().follow_redirects=False.allow_remote_media_redirectsis 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_resolved_url()..get().Files changed
nanobot/channels/dingtalk.pytests/channels/test_dingtalk_channel.pyMaintainer impact
allow_remote_media_redirects: truefor the DingTalk channel.remote_media_redirect_allowed_hosts.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
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 --checkResult:
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.