@@ -2,6 +2,87 @@ use crate::{StdFunction, StdlibModule, StdlibRegistry};
22use std:: collections:: HashMap ;
33use std:: rc:: Rc ;
44use std:: sync:: atomic:: { AtomicBool , Ordering } ;
5+ use std:: net:: { ToSocketAddrs , IpAddr , SocketAddr } ;
6+ use url:: Url ;
7+ use ureq:: Resolver ;
8+
9+ fn is_safe_ip ( ip : & IpAddr ) -> bool {
10+ match ip {
11+ IpAddr :: V4 ( ipv4) => {
12+ !ipv4. is_private ( )
13+ && !ipv4. is_loopback ( )
14+ && !ipv4. is_link_local ( )
15+ && !ipv4. is_broadcast ( )
16+ && !ipv4. is_documentation ( )
17+ && !ipv4. is_unspecified ( )
18+ }
19+ IpAddr :: V6 ( ipv6) => {
20+ if let Some ( ipv4) = ipv6. to_ipv4 ( ) {
21+ // Check IPv4-mapped IPv6
22+ return is_safe_ip ( & IpAddr :: V4 ( ipv4) ) ;
23+ }
24+ !ipv6. is_loopback ( )
25+ && !ipv6. is_unspecified ( )
26+ // IPv6 specific checks
27+ && ( ipv6. segments ( ) [ 0 ] & 0xfe00 ) != 0xfc00 // Unique Local Address
28+ && ( ipv6. segments ( ) [ 0 ] & 0xffc0 ) != 0xfe80 // Link Local Address
29+ }
30+ }
31+ }
32+
33+ fn is_safe_url ( url_str : & str ) -> bool {
34+ let Ok ( parsed_url) = Url :: parse ( url_str) else {
35+ return false ; // Invalid URL
36+ } ;
37+
38+ match parsed_url. scheme ( ) {
39+ "http" | "https" => { }
40+ _ => return false , // Block file://, ftp://, gopher://, etc.
41+ }
42+
43+ let host = match parsed_url. host_str ( ) {
44+ Some ( h) => h,
45+ None => return false , // No host provided
46+ } ;
47+
48+ let port = parsed_url. port_or_known_default ( ) . unwrap_or ( 80 ) ;
49+ let addr_str = format ! ( "{}:{}" , host, port) ;
50+
51+ // Resolve the domain to IPs
52+ let addrs = match addr_str. to_socket_addrs ( ) {
53+ Ok ( a) => a,
54+ Err ( _) => return false , // DNS resolution failed
55+ } ;
56+
57+ for addr in addrs {
58+ if !is_safe_ip ( & addr. ip ( ) ) {
59+ return false ; // Found an unsafe IP
60+ }
61+ }
62+
63+ true
64+ }
65+
66+ struct SafeResolver ;
67+
68+ impl Resolver for SafeResolver {
69+ fn resolve ( & self , netloc : & str ) -> std:: io:: Result < Vec < SocketAddr > > {
70+ let addrs: Vec < SocketAddr > = netloc. to_socket_addrs ( ) ?. collect ( ) ;
71+ let mut safe_addrs = Vec :: new ( ) ;
72+ for addr in addrs {
73+ if is_safe_ip ( & addr. ip ( ) ) {
74+ safe_addrs. push ( addr) ;
75+ }
76+ }
77+ if safe_addrs. is_empty ( ) {
78+ return Err ( std:: io:: Error :: new (
79+ std:: io:: ErrorKind :: PermissionDenied ,
80+ "DNS resolution returned only blocked/internal IP addresses (SSRF prevention)." ,
81+ ) ) ;
82+ }
83+ Ok ( safe_addrs)
84+ }
85+ }
586use std:: sync:: Mutex ;
687use std:: thread;
788use techscript_runtime:: {
@@ -432,7 +513,22 @@ impl StdlibRegistry {
432513 arity : 1 ,
433514 callback : |_ctx, args| {
434515 let url = args[ 0 ] . to_string ( ) ;
435- let body = ureq:: get ( & url)
516+
517+ if !is_safe_url ( & url) {
518+ return Err ( RuntimeError :: new (
519+ techscript_runtime:: error:: RuntimeErrorKind :: InvalidOperation (
520+ format ! ( "Access denied: the URL '{}' points to a blocked or internal destination (SSRF prevention)." , url)
521+ ) ,
522+ None ,
523+ None ,
524+ ) ) ;
525+ }
526+
527+ let agent = ureq:: builder ( )
528+ . resolver ( SafeResolver )
529+ . redirects ( 0 )
530+ . build ( ) ;
531+ let body = agent. get ( & url)
436532 . call ( )
437533 . map_err ( |e| {
438534 RuntimeError :: new (
0 commit comments