Binding
Shell core / Not binding-specific
Problem Statement
The SSRF guard works as documented: curl refuses RFC 1918, link-local,
loopback and IMDS addresses and lets everything else through, and allowed_urls
adds exceptions to that list. Two things are hard to do with that model when the
shell is embedded in a service running inside a cloud VPC:
-
There is no way to say "no network at all". allowed_urls only widens
access; there is no deny-list, no network: false, and no way to leave
curl out of the command table. An embedder who wants a filesystem plus
jq/lua for data processing, with no egress, cannot express that.
-
Addresses in 100.64.0.0/10 are treated as public. RFC 6598 "shared
address space" (carrier-grade NAT) is not routable on the public internet,
and it is common as a Kubernetes pod or secondary VPC CIDR precisely because
RFC 1918 space runs out: kOps used it as the default pod network, AWS
documents it as the secondary CIDR for EKS custom networking, and Tailscale
uses it too. Internal load balancers in such clusters resolve into it, so
from a pod inside the cluster curl can reach every internal service fronted
by one of those addresses, unauthenticated where the service trusts the
network perimeter.
import strands_shell
s = strands_shell.Shell(timeout=5)
print(s.run("curl -s http://10.0.0.1/").stderr) # "curl: access denied: 10.0.0.1", exit 1
print(s.run("curl -s http://[fc00::1]/").stderr) # "curl: access denied: fc00::1", exit 1
print(s.run("curl -s http://100.64.0.1/").status) # exit 6: connect attempted, not refused
s = strands_shell.Shell(timeout=5, allowed_urls=["https://example.com"])
print(s.run("curl -s http://100.64.0.1/").status) # exit 6: the allowlist restricts nothing
Observed on 0.3.3 (Python, macOS arm64 and Linux x86_64); main at b4290ca has
the same is_ip_blocked.
Proposed Solution
-
A builder option to disable network access, e.g. .network(false) in Rust,
network_enabled=False / networkEnabled: false in the Python and Node
bindings and a top-level network = false in the TOML config. It would make
check_url refuse every URL (or leave curl out of the command table) and be
reported in Shell.config.
-
Add RFC 6598 to is_ip_blocked in src/vfs_kernel.rs:
// RFC 6598 shared address space (CGNAT), 100.64.0.0/10
|| (v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64)
(Ipv4Addr::is_shared() is the same test but may still be behind the ip
feature gate.) The README bullet would then read "RFC 1918, RFC 6598,
link-local, loopback, IMDS".
-
Optionally, a denied_urls list mirroring allowed_urls, for embedders who
need public access but want to fence off specific internal zones (an IPv6 VPC
prefix, an internal DNS suffix) that no generic blocklist can know about.
Use Case
An agent platform runs Strands Shell in-process inside a Kubernetes pod that
legitimately needs the cluster's internal APIs and a model provider. The shell is
given to LLM agents as a data-processing tool: results are staged into the VFS and
reduced with jq/lua. Those agents read untrusted text (issue trackers, merge
request descriptions, web pages), so a prompt-injected curl -X POST http://<internal-lb>/... is a realistic path, and the pod's network reach is
exactly what the agent inherits.
With network_enabled=False the platform could grant the shell without granting
egress. With RFC 6598 in the blocklist, the documented "private ranges" promise
would hold on the clusters described above even for embedders who keep public
curl.
Alternatives Solutions
Things an embedder can try today, and why they do not hold:
- Shadow
curl with a shell function. Catches direct and indirect calls in
the shell, but is undone by unset -f curl and does not apply to Lua's
os.execute / io.popen, which run outside the function table.
- Remove
/bin/curl. It is a builtin; the VFS entry is read-only even via
the host-side remove_file, and alias is never expanded.
- Reject commands containing
curl before run(). Defeated by string
construction (c=cu; d=rl; $c$d, or the same in Lua).
- Point the client at a black-hole proxy. The reqwest client is built per
request from the process environment, so HTTPS_PROXY would also redirect
the host process's own HTTP clients.
- Network policy. The shell shares the pod's network namespace, and the pod
needs the same internal endpoints, so policy cannot separate the two.
- Run each shell in a container or microVM, as the README recommends for
adversarial workloads. Correct, but it gives up the in-process embedding that
is the library's main appeal; a network switch would cover the common case
without it.
Additional Context
The IPv6 side has the same shape and no blocklist answer: pods in an IPv6
cluster carry global unicast addresses (e.g. 2600::/12 on AWS), which no "is it
private?" test can recognise as internal. That is the main reason to prefer a
hard off switch over an ever-longer list.
Binding
Shell core / Not binding-specific
Problem Statement
The SSRF guard works as documented:
curlrefuses RFC 1918, link-local,loopback and IMDS addresses and lets everything else through, and
allowed_urlsadds exceptions to that list. Two things are hard to do with that model when the
shell is embedded in a service running inside a cloud VPC:
There is no way to say "no network at all".
allowed_urlsonly widensaccess; there is no deny-list, no
network: false, and no way to leavecurlout of the command table. An embedder who wants a filesystem plusjq/luafor data processing, with no egress, cannot express that.Addresses in 100.64.0.0/10 are treated as public. RFC 6598 "shared
address space" (carrier-grade NAT) is not routable on the public internet,
and it is common as a Kubernetes pod or secondary VPC CIDR precisely because
RFC 1918 space runs out: kOps used it as the default pod network, AWS
documents it as the secondary CIDR for EKS custom networking, and Tailscale
uses it too. Internal load balancers in such clusters resolve into it, so
from a pod inside the cluster
curlcan reach every internal service frontedby one of those addresses, unauthenticated where the service trusts the
network perimeter.
Observed on 0.3.3 (Python, macOS arm64 and Linux x86_64);
mainat b4290ca hasthe same
is_ip_blocked.Proposed Solution
A builder option to disable network access, e.g.
.network(false)in Rust,network_enabled=False/networkEnabled: falsein the Python and Nodebindings and a top-level
network = falsein the TOML config. It would makecheck_urlrefuse every URL (or leavecurlout of the command table) and bereported in
Shell.config.Add RFC 6598 to
is_ip_blockedinsrc/vfs_kernel.rs:(
Ipv4Addr::is_shared()is the same test but may still be behind theipfeature gate.) The README bullet would then read "RFC 1918, RFC 6598,
link-local, loopback, IMDS".
Optionally, a
denied_urlslist mirroringallowed_urls, for embedders whoneed public access but want to fence off specific internal zones (an IPv6 VPC
prefix, an internal DNS suffix) that no generic blocklist can know about.
Use Case
An agent platform runs Strands Shell in-process inside a Kubernetes pod that
legitimately needs the cluster's internal APIs and a model provider. The shell is
given to LLM agents as a data-processing tool: results are staged into the VFS and
reduced with
jq/lua. Those agents read untrusted text (issue trackers, mergerequest descriptions, web pages), so a prompt-injected
curl -X POST http://<internal-lb>/...is a realistic path, and the pod's network reach isexactly what the agent inherits.
With
network_enabled=Falsethe platform could grant the shell without grantingegress. With RFC 6598 in the blocklist, the documented "private ranges" promise
would hold on the clusters described above even for embedders who keep public
curl.Alternatives Solutions
Things an embedder can try today, and why they do not hold:
curlwith a shell function. Catches direct and indirect calls inthe shell, but is undone by
unset -f curland does not apply to Lua'sos.execute/io.popen, which run outside the function table./bin/curl. It is a builtin; the VFS entry is read-only even viathe host-side
remove_file, andaliasis never expanded.curlbeforerun(). Defeated by stringconstruction (
c=cu; d=rl; $c$d, or the same in Lua).request from the process environment, so
HTTPS_PROXYwould also redirectthe host process's own HTTP clients.
needs the same internal endpoints, so policy cannot separate the two.
adversarial workloads. Correct, but it gives up the in-process embedding that
is the library's main appeal; a network switch would cover the common case
without it.
Additional Context
The IPv6 side has the same shape and no blocklist answer: pods in an IPv6
cluster carry global unicast addresses (e.g. 2600::/12 on AWS), which no "is it
private?" test can recognise as internal. That is the main reason to prefer a
hard off switch over an ever-longer list.