Skip to content

Commit 2d656d4

Browse files
Block procedures from requesting private ip ranges (#4243)
# Description of Changes Blocks procedures from requesting private ip ranges after dns resolution. Adds a new cargo feature to `spacetimedb-standalone` permitting loopback http requests in test environments only. # API and ABI breaking changes None # Expected complexity level and risk 2. I may have missed a range. # Testing - [x] Unit tests for IP address matching - [x] Smoketests for blocking a private IP address
1 parent b41a0a5 commit 2d656d4

12 files changed

Lines changed: 843 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ jobs:
903903
export CARGO_HOME="$HOME/.cargo"
904904
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
905905
cargo install --force --path crates/cli --locked --message-format=short
906-
cargo install --force --path crates/standalone --locked --message-format=short
906+
cargo install --force --path crates/standalone --features allow_loopback_http_for_tests --locked --message-format=short
907907
# Add a handy alias using the old binary name, so that we don't have to rewrite all scripts (incl. in submodules).
908908
ln -sf $CARGO_HOME/bin/spacetimedb-cli $CARGO_HOME/bin/spacetime
909909

crates/core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ nix = { workspace = true, features = ["sched"] }
137137
# Print a warning when doing an unindexed `iter_by_col_range` on a large table.
138138
unindexed_iter_by_col_range_warn = []
139139
default = ["unindexed_iter_by_col_range_warn"]
140+
# Test-only escape hatch used by SDK procedure tests that intentionally call `localhost`.
141+
# Keep this off in production builds.
142+
allow_loopback_http_for_tests = []
140143
# Enable timing for wasm ABI calls
141144
spacetimedb-wasm-instance-env-times = []
142145
# Enable test helpers and utils

crates/core/src/host/instance_env.rs

Lines changed: 707 additions & 1 deletion
Large diffs are not rendered by default.

crates/guard/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub fn ensure_binaries_built() -> PathBuf {
7979
\n\
8080
Or build manually:\n\
8181
\n\
82-
cargo build -p spacetimedb-cli -p spacetimedb-standalone\n\
82+
cargo build -p spacetimedb-cli -p spacetimedb-standalone --features spacetimedb-standalone/allow_loopback_http_for_tests\n\
8383
========================================================================\n",
8484
cli_path.display()
8585
);

crates/smoketests/DEVELOP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ you MUST rebuild before running tests:
3030
cargo smoketest
3131

3232
# Option 2: Manually rebuild, then run tests directly
33-
cargo build -p spacetimedb-cli -p spacetimedb-standalone
33+
cargo build -p spacetimedb-cli -p spacetimedb-standalone --features spacetimedb-standalone/allow_loopback_http_for_tests
3434
cargo nextest run -p spacetimedb-smoketests
3535
```
3636

@@ -54,7 +54,7 @@ Pre-building avoids this entirely.
5454
Standard `cargo test` also works, but you must rebuild first:
5555

5656
```bash
57-
cargo build -p spacetimedb-cli -p spacetimedb-standalone
57+
cargo build -p spacetimedb-cli -p spacetimedb-standalone --features spacetimedb-standalone/allow_loopback_http_for_tests
5858
cargo test -p spacetimedb-smoketests
5959
```
6060

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use std::io::Write;
2+
use std::net::TcpListener;
3+
use std::thread::JoinHandle;
4+
use std::time::{Duration, Instant};
5+
6+
use spacetimedb_smoketests::Smoketest;
7+
8+
fn module_code_http_disallowed_ip(addr: &str, port: u16) -> String {
9+
format!(
10+
r#"
11+
use spacetimedb::ProcedureContext;
12+
13+
#[spacetimedb::procedure]
14+
pub fn request_disallowed_ip(ctx: &mut ProcedureContext) -> Result<(), String> {{
15+
match ctx.http.get("http://{addr}:{port}/") {{
16+
Ok(_) => Err("request unexpectedly succeeded".to_owned()),
17+
Err(err) => {{
18+
let message = err.to_string();
19+
if message.contains("refusing to connect to private or special-purpose addresses") {{
20+
Ok(())
21+
}} else {{
22+
Err(format!("unexpected error from http request: {{message}}"))
23+
}}
24+
}}
25+
}}
26+
}}
27+
"#
28+
)
29+
}
30+
31+
fn spawn_redirect_server(location: &str) -> (u16, JoinHandle<std::io::Result<()>>) {
32+
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("failed to bind test redirect server");
33+
listener
34+
.set_nonblocking(true)
35+
.expect("failed to set test redirect server nonblocking mode");
36+
let port = listener
37+
.local_addr()
38+
.expect("failed to read test redirect server address")
39+
.port();
40+
let location = location.to_owned();
41+
let handle = std::thread::spawn(move || -> std::io::Result<()> {
42+
let deadline = Instant::now() + Duration::from_secs(10);
43+
let (mut stream, _) = loop {
44+
match listener.accept() {
45+
Ok(pair) => break pair,
46+
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
47+
if Instant::now() >= deadline {
48+
return Err(std::io::Error::new(
49+
std::io::ErrorKind::TimedOut,
50+
"redirect test server did not receive a request; rebuild standalone with allow_loopback_http_for_tests",
51+
));
52+
}
53+
std::thread::sleep(Duration::from_millis(10));
54+
}
55+
Err(err) => return Err(err),
56+
}
57+
};
58+
let response =
59+
format!("HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
60+
stream.write_all(response.as_bytes())?;
61+
stream.flush()?;
62+
Ok(())
63+
});
64+
(port, handle)
65+
}
66+
67+
#[test]
68+
fn test_http_disallowed_ip_is_blocked() {
69+
let module_code = module_code_http_disallowed_ip("10.0.0.1", 80);
70+
let test = Smoketest::builder().module_code(&module_code).build();
71+
72+
let output = test.call_output("request_disallowed_ip", &[]);
73+
let stdout = String::from_utf8_lossy(&output.stdout);
74+
let stderr = String::from_utf8_lossy(&output.stderr);
75+
assert!(
76+
output.status.success(),
77+
"Expected request_disallowed_ip to succeed after observing blocked egress error.\nstdout:\n{}\nstderr:\n{}",
78+
stdout,
79+
stderr
80+
);
81+
}
82+
83+
#[test]
84+
fn test_http_redirect_to_disallowed_ip_is_blocked() {
85+
let (port, redirect_server) = spawn_redirect_server("http://10.0.0.1:80/");
86+
let module_code = module_code_http_disallowed_ip("localhost", port);
87+
let test = Smoketest::builder().module_code(&module_code).build();
88+
89+
let output = test.call_output("request_disallowed_ip", &[]);
90+
let stdout = String::from_utf8_lossy(&output.stdout);
91+
let stderr = String::from_utf8_lossy(&output.stderr);
92+
assert!(
93+
output.status.success(),
94+
"Expected request_disallowed_ip to succeed after observing blocked egress error.\nstdout:\n{}\nstderr:\n{}",
95+
stdout,
96+
stderr
97+
);
98+
99+
redirect_server
100+
.join()
101+
.expect("redirect test server thread panicked")
102+
.expect("redirect test server failed");
103+
}

crates/smoketests/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod domains;
1818
pub mod energy;
1919
pub mod fail_initial_publish;
2020
pub mod filtering;
21+
pub mod http_egress;
2122
pub mod module_nested_op;
2223
pub mod modules;
2324
pub mod namespaces;

crates/standalone/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ required-features = [] # Features required to build this target (N/A for lib)
1818

1919
[features]
2020
unstable = ["spacetimedb-client-api/unstable"]
21+
allow_loopback_http_for_tests = ["spacetimedb-core/allow_loopback_http_for_tests"]
2122
# Perfmaps for profiling modules
2223
perfmap = ["spacetimedb-core/perfmap"]
2324
# Disables core pinning

crates/testing/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ edition.workspace = true
55
license-file = "LICENSE"
66
publish = false
77

8+
[features]
9+
allow_loopback_http_for_tests = ["spacetimedb-standalone/allow_loopback_http_for_tests"]
10+
811
[dependencies]
912
spacetimedb-cli.workspace = true
1013
spacetimedb-data-structures.workspace = true

sdks/rust/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ rust-version.workspace = true
88

99
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1010

11+
[features]
12+
allow_loopback_http_for_tests = ["spacetimedb-testing/allow_loopback_http_for_tests"]
13+
1114
[dependencies]
1215
spacetimedb-data-structures.workspace = true
1316
spacetimedb-sats.workspace = true

0 commit comments

Comments
 (0)