Skip to content

Commit afa6af7

Browse files
JSKittyclaude
andcommitted
refactor: deduplicate net.rs SSRF protection, use vector-core
Remove duplicated validate_url_not_private + is_ipv6_private from src-tauri, re-export from vector-core. Tauri-specific functions (download, fetch_site_metadata) remain in src-tauri. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4ad7795 commit afa6af7

1 file changed

Lines changed: 1 addition & 59 deletions

File tree

src-tauri/src/net.rs

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,70 +4,12 @@ use futures_util::StreamExt;
44
use reqwest::{self, Client};
55
use serde_json::json;
66
use tauri::{AppHandle, Emitter};
7-
use url::Url;
87

8+
pub use vector_core::net::validate_url_not_private;
99
pub use vector_core::SiteMetadata;
1010

1111
use crate::simd::html_meta;
1212

13-
/// Reject URLs that resolve to private/loopback/link-local addresses (SSRF protection).
14-
/// Returns Ok(()) if the URL is safe to fetch, Err with reason otherwise.
15-
pub fn validate_url_not_private(url_str: &str) -> Result<(), &'static str> {
16-
let parsed = Url::parse(url_str).map_err(|_| "Invalid URL")?;
17-
18-
// Only allow http/https schemes
19-
match parsed.scheme() {
20-
"http" | "https" => {}
21-
_ => return Err("Only HTTP(S) URLs are allowed"),
22-
}
23-
24-
// Check the host — reject IPs directly, resolve hostnames later (reqwest handles DNS)
25-
match parsed.host() {
26-
Some(url::Host::Ipv4(ip)) => {
27-
let o = ip.octets();
28-
if ip.is_loopback()
29-
|| ip.is_private()
30-
|| ip.is_link_local()
31-
|| ip.is_broadcast()
32-
|| ip.is_unspecified()
33-
|| (o[0] == 100 && o[1] >= 64 && o[1] <= 127) // CGN (100.64.0.0/10)
34-
{
35-
return Err("Private/internal IP addresses are not allowed");
36-
}
37-
}
38-
Some(url::Host::Ipv6(ip)) => {
39-
if ip.is_loopback() || ip.is_unspecified() || is_ipv6_private(&ip) {
40-
return Err("Private/internal IP addresses are not allowed");
41-
}
42-
}
43-
Some(url::Host::Domain(domain)) => {
44-
// Block localhost variants
45-
if domain == "localhost"
46-
|| domain.ends_with(".local")
47-
|| domain.ends_with(".internal")
48-
{
49-
return Err("Local hostnames are not allowed");
50-
}
51-
}
52-
None => return Err("URL has no host"),
53-
}
54-
55-
Ok(())
56-
}
57-
58-
fn is_ipv6_private(ip: &std::net::Ipv6Addr) -> bool {
59-
// Check for IPv4-mapped addresses (::ffff:x.x.x.x)
60-
if let Some(ipv4) = ip.to_ipv4_mapped() {
61-
return ipv4.is_loopback() || ipv4.is_private() || ipv4.is_link_local();
62-
}
63-
let segments = ip.segments();
64-
// Unique local (fc00::/7)
65-
if segments[0] & 0xfe00 == 0xfc00 { return true; }
66-
// Link-local (fe80::/10)
67-
if segments[0] & 0xffc0 == 0xfe80 { return true; }
68-
false
69-
}
70-
7113
/// Trait for reporting download progress
7214
pub trait ProgressReporter {
7315
/// Report progress of a download

0 commit comments

Comments
 (0)