Skip to content

Commit f42353a

Browse files
committed
fix(sandbox): treat literal IP in policy host as implicit allowed_ips
When a policy endpoint specifies a literal IP address as the host (e.g. host: 192.168.86.157), the user has explicitly declared intent to allow that destination. The SSRF guard requiring allowed_ips was redundant for this case and forced users to duplicate the IP. Synthesize an implicit allowed_ips entry when the host parses as an IP address, so the existing allowlist-validation path is used instead of the blanket internal-IP rejection. Loopback and link-local addresses remain blocked by resolve_and_check_allowed_ips. Applies to both the CONNECT and FORWARD proxy paths. Refs: #567
1 parent f37b69b commit f42353a

File tree

1 file changed

+51
-2
lines changed

1 file changed

+51
-2
lines changed

crates/openshell-sandbox/src/proxy.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,12 @@ async fn handle_tcp_connection(
418418
// Query allowed_ips from the matched endpoint config (if any).
419419
// When present, the SSRF check validates resolved IPs against this
420420
// allowlist instead of blanket-blocking all private IPs.
421-
let raw_allowed_ips = query_allowed_ips(&opa_engine, &decision, &host_lc, port);
421+
// When the policy host is already a literal IP address, treat it as
422+
// implicitly allowed — the user explicitly declared the destination.
423+
let mut raw_allowed_ips = query_allowed_ips(&opa_engine, &decision, &host_lc, port);
424+
if raw_allowed_ips.is_empty() {
425+
raw_allowed_ips = implicit_allowed_ips_for_ip_host(&host);
426+
}
422427

423428
// Defense-in-depth: resolve DNS and reject connections to internal IPs.
424429
let mut upstream = if !raw_allowed_ips.is_empty() {
@@ -1236,6 +1241,19 @@ fn is_internal_ip(ip: IpAddr) -> bool {
12361241
}
12371242
}
12381243

1244+
/// When the policy endpoint host is a literal IP address, the user has
1245+
/// explicitly declared intent to allow that destination. Synthesize an
1246+
/// `allowed_ips` entry so the existing allowlist-validation path is used
1247+
/// instead of the blanket internal-IP rejection. Loopback and link-local
1248+
/// addresses are still blocked by `resolve_and_check_allowed_ips`.
1249+
fn implicit_allowed_ips_for_ip_host(host: &str) -> Vec<String> {
1250+
if host.parse::<IpAddr>().is_ok() {
1251+
vec![host.to_string()]
1252+
} else {
1253+
vec![]
1254+
}
1255+
}
1256+
12391257
/// Resolve DNS for a host:port and reject if any resolved address is internal.
12401258
///
12411259
/// Returns the resolved `SocketAddr` list on success. Returns an error string
@@ -1806,7 +1824,12 @@ async fn handle_forward_proxy(
18061824
// - If allowed_ips is set: validate resolved IPs against the allowlist
18071825
// (this is the SSRF override for private IP destinations).
18081826
// - If allowed_ips is empty: reject internal IPs, allow public IPs through.
1809-
let raw_allowed_ips = query_allowed_ips(&opa_engine, &decision, &host_lc, port);
1827+
// When the policy host is already a literal IP address, treat it as
1828+
// implicitly allowed — the user explicitly declared the destination.
1829+
let mut raw_allowed_ips = query_allowed_ips(&opa_engine, &decision, &host_lc, port);
1830+
if raw_allowed_ips.is_empty() {
1831+
raw_allowed_ips = implicit_allowed_ips_for_ip_host(&host);
1832+
}
18101833

18111834
let addrs = if !raw_allowed_ips.is_empty() {
18121835
// allowed_ips mode: validate resolved IPs against CIDR allowlist.
@@ -2743,4 +2766,30 @@ mod tests {
27432766
"expected 'always-blocked' in error: {err}"
27442767
);
27452768
}
2769+
2770+
// -- implicit_allowed_ips_for_ip_host --
2771+
2772+
#[test]
2773+
fn test_implicit_allowed_ips_returns_ip_for_ipv4_literal() {
2774+
let result = implicit_allowed_ips_for_ip_host("192.168.1.100");
2775+
assert_eq!(result, vec!["192.168.1.100"]);
2776+
}
2777+
2778+
#[test]
2779+
fn test_implicit_allowed_ips_returns_ip_for_ipv6_literal() {
2780+
let result = implicit_allowed_ips_for_ip_host("::1");
2781+
assert_eq!(result, vec!["::1"]);
2782+
}
2783+
2784+
#[test]
2785+
fn test_implicit_allowed_ips_returns_empty_for_hostname() {
2786+
let result = implicit_allowed_ips_for_ip_host("api.github.com");
2787+
assert!(result.is_empty());
2788+
}
2789+
2790+
#[test]
2791+
fn test_implicit_allowed_ips_returns_empty_for_wildcard() {
2792+
let result = implicit_allowed_ips_for_ip_host("*.example.com");
2793+
assert!(result.is_empty());
2794+
}
27462795
}

0 commit comments

Comments
 (0)