Skip to content

fix(transcribe): block shorthand IPv4 spellings of internal hosts - #564

Closed
SEPURI-SAI-KRISHNA wants to merge 1 commit into
Panniantong:mainfrom
SEPURI-SAI-KRISHNA:fix/transcribe-ssrf-shorthand-ipv4
Closed

fix(transcribe): block shorthand IPv4 spellings of internal hosts#564
SEPURI-SAI-KRISHNA wants to merge 1 commit into
Panniantong:mainfrom
SEPURI-SAI-KRISHNA:fix/transcribe-ssrf-shorthand-ipv4

Conversation

@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown
Contributor

Problem

_assert_safe_public_url() in agent_reach/transcribe.py is the SSRF guard added by #443 (closing #444). It rejects internal targets by parsing the host with ipaddress.ip_address() — which only accepts the canonical dotted-quad form.

The C resolver behind yt-dlp accepts the whole inet_aton grammar. So every non-canonical spelling of an internal address walks straight past the check:

URL passed to transcribe Guard says Actually reaches
http://127.1/ allowed 127.0.0.1
http://127.0.1/ allowed 127.0.0.1
http://2130706433/ allowed 127.0.0.1
http://0x7f000001/ allowed 127.0.0.1
http://0177.0.0.1/ allowed 127.0.0.1
http://0/ allowed 0.0.0.0
http://192.168.1/ allowed 192.168.0.1
http://2852039166/ allowed 169.254.169.254
http://0xA9FEA9FE/ allowed 169.254.169.254

The last two reach the cloud instance-metadata endpoint — the exact target the guard names in its own _BLOCKED_HOSTS neighbourhood (metadata.google.internal). http://169.254.169.254/ is blocked; http://0xA9FEA9FE/ is not.

Reproduce on main:

from agent_reach.transcribe import _assert_safe_public_url
_assert_safe_public_url("http://0xA9FEA9FE/")   # returns — no exception
import socket
socket.getaddrinfo("0xA9FEA9FE", 80)[0][4]      # ('169.254.169.254', 80)

This matters because transcribe(source) takes its URL from agent-reach transcribe <source> and from YouTubeChannel.transcribe(), i.e. from an agent or from scraped content — precisely the untrusted-input path #444 was filed about.

Fix

Parse the host with the same grammar the resolver uses, then run the existing private/loopback/link-local/reserved/multicast/unspecified check against the canonicalised address.

socket.inet_aton is the same libc parser getaddrinfo uses for IPv4 literals, and it rejects real hostnames (example.com, localhost, 1.2.3.4.5OSError), so it is a pure literal parse.

No DNS lookup is added. The existing test_does_not_dns_resolve_public_hostnames contract is preserved, and a new test asserts the shorthand path stays DNS-free by making socket.getaddrinfo raise.

Public literal addresses are unaffected — including http://010.010.010.010/, which is octal for the public 8.8.8.8 and stays allowed.

Tests

tests/test_transcribe.py, in the existing TestDownloadAudioSafety class:

  • test_rejects_shorthand_ipv4_spellings_of_internal_hosts — 10 parametrised spellings, each asserting yt-dlp is never invoked
  • test_shorthand_ipv4_check_stays_dns_free — fails if the new parsing resolves a name
  • test_allows_public_literal_addresses — regression guard for 1.1.1.1, 8.8.8.8, 010.010.010.010

pytest -q: 442 passed (was 428). ruff check clean on both touched files.

Scope

Deliberately narrow — one function, no behaviour change for any valid input. It does not touch the separate WebChannel SSRF discussion in #456/#370, and does not add redirect-following or DNS-rebinding protection; those remain out of scope for this guard.

@Panniantong

Copy link
Copy Markdown
Owner

感谢发现 shorthand IPv4 SSRF 绕过。你的原始作者提交已保留在 merged #577 中,并扩展覆盖 Unicode IDNA、percent-encoded host 与反斜杠 authority,完整 CI 已通过,因此关闭原 PR。

@Panniantong Panniantong closed this Aug 6, 2026
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