Serve preview files only to requests addressed to loopback (R2.9) (from #600)

This commit is contained in:
Afloat
2026-11-03 19:53:21 +07:00
committed by bflatastic
parent 96627da921
commit b516e2a313
2 changed files with 68 additions and 1 deletions
+18 -1
View File
@@ -134,9 +134,26 @@ fn handle(mut stream: TcpStream, root: &Path) -> std::io::Result<()> {
reader.read_line(&mut line)?;
// Drain the headers (bounded by `take`).
let mut h = String::new();
let (mut hosts, mut origins) = (Vec::new(), Vec::new());
while reader.read_line(&mut h)? > 2 {
if let Some((name, value)) = h.trim_end_matches(['\r', '\n']).split_once(':') {
if name.eq_ignore_ascii_case("host") {
hosts.push(value.trim().to_string());
}
if name.eq_ignore_ascii_case("origin") {
origins.push(value.trim().to_string());
}
}
h.clear();
}
if hosts.len() != 1 || origins.len() > 1 {
return stream.write_all(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
}
if !hosts.first().is_some_and(|h| crate::standin_server::host_is_loopback(h, Some(stream.local_addr().map_or(0, |a| a.port()))))
|| origins.first().is_some_and(|o| !crate::standin_server::origin_is_local(o))
{
return stream.write_all(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
}
let mut parts = line.split_whitespace();
let (method, target) = (parts.next().unwrap_or_default(), parts.next().unwrap_or("/"));
if method != "GET" && method != "HEAD" {
@@ -176,7 +193,7 @@ mod tests {
fn get(port: u16, path: &str) -> String {
let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
s.write_all(format!("GET {path} HTTP/1.1\r\nHost: x\r\n\r\n").as_bytes()).unwrap();
s.write_all(format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n\r\n").as_bytes()).unwrap();
let mut out = String::new();
s.read_to_string(&mut out).unwrap();
out
@@ -0,0 +1,50 @@
use craft_anything_platform::serve::serve_dir;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
fn get(port: u16, path: &str) -> String {
let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
s.write_all(format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n\r\n").as_bytes()).unwrap();
let mut out = String::new();
s.read_to_string(&mut out).unwrap();
out
}
#[test]
fn regression_preview_refuses_foreign_hosts_and_origins() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("index.html"), "local-only-content").unwrap();
let srv = serve_dir(dir.path()).unwrap();
for extra in [
"Host: attacker.example\r\n".to_string(),
"Host: localhost.attacker.example\r\n".to_string(),
format!("Host: localhost:{}\r\nOrigin: https://attacker.example\r\n", srv.port),
format!("Host: localhost:{}\r\nHost: attacker.example\r\n", srv.port),
format!("Host: localhost:{}\r\nOrigin: http://localhost:3000\r\nOrigin: https://attacker.example\r\n", srv.port),
] {
let mut s = TcpStream::connect(("127.0.0.1", srv.port)).unwrap();
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
s.write_all(format!("GET / HTTP/1.1\r\n{extra}\r\n").as_bytes()).unwrap();
let mut out = String::new();
s.read_to_string(&mut out).unwrap();
assert!(out.starts_with("HTTP/1.1 400") || out.starts_with("HTTP/1.1 403"), "{out}");
assert!(!out.contains("local-only-content"));
}
assert!(get(srv.port, "/").contains("local-only-content"));
}
#[test]
fn regression_preview_accepts_a_local_origin() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("index.html"), "local-only-content").unwrap();
let srv = serve_dir(dir.path()).unwrap();
let mut stream = TcpStream::connect(("127.0.0.1", srv.port)).unwrap();
stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
stream.write_all(format!("GET / HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nOrigin: http://localhost:3000\r\n\r\n", srv.port).as_bytes()).unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
let (headers, body) = response.split_once("\r\n\r\n").unwrap();
assert!(headers.starts_with("HTTP/1.1 200 "), "{headers}");
assert_eq!(body, "local-only-content");
}